Vantage logoVantage
FeaturesPricingBlogChangelog
Sign inGet started

Changelog

Latest improvements, features, and fixes.

Jump to an update
Jul 30, 2026The walkthrough now has you use every feature, and catches it when one failsJul 30, 2026The walkthrough said tailoring logs the application for youJul 30, 2026The walkthrough explains again instead of making you do thingsJul 30, 2026Pro is counted per month like Free, and no longer sold as unlimitedJul 29, 2026Downloaded resumes now keep their layout, links stay clickable, and the preview hides raw URLsJul 29, 2026Section titles no longer render as bullets, and deduping stopped taking unique content with itJul 29, 2026Project and publication titles were being deleted, and a publication was promoted to a section of its ownJul 29, 2026Restructuring rules generalised beyond the one resume they were written againstJul 29, 2026Structure is now inferred from the resume rather than matched against a list of known formatsJul 29, 2026Empty title boxes in the preview, and a prompt rule against merging bulletsJul 29, 2026Link detection now uses where text sits rather than a list of TLDsJul 29, 2026Corrupted dates in the PDF, and a publication losing its titleJul 29, 2026Education entries can carry their own lines, so coursework stops becoming a section of its ownJul 29, 2026A resume keeps its own section order instead of being rearranged into Vantage'sJul 29, 2026Content filed under the wrong adjacent heading is put back where the resume had itJul 29, 2026A resume keeps its own wording for its section headingsJul 29, 2026No two sections can print the same headingJul 29, 2026Content printed inside a section now stays inside it, instead of becoming an unnamed sectionJul 29, 2026The resume format now has no fixed sections - a resume's sections are whatever the candidate wroteJul 29, 2026A resume saved by an older build no longer crashes the editorJul 29, 2026Dates print against the right margin, and a transcription that drops a bullet is caught and retriedJul 29, 2026A Word resume is no longer rebuilt - it is edited, so the download is your own documentJul 29, 2026Tailored downloads keep the link to the user's own documentJul 29, 2026Resume Studio downloads the user's own file too, the way tailoring doesJul 29, 2026Undo, redo and Save in Resume Studio, and a prompt to upload the Word version of your resumeJul 29, 2026Loading spinners were invisible in light modeJul 29, 2026Page titles are consistent across every page in the appJul 29, 2026The upload-your-Word-resume card fills the width it is givenJul 29, 2026Long waits say what is happening instead of spinningJul 29, 2026A guided walkthrough of the app on first sign-inJul 29, 2026The walkthrough now has you use the app, and three bugs in itJul 29, 2026Replay the walkthrough did nothingJul 29, 2026The walkthrough now follows the tailor page's actual flowJul 29, 2026The walkthrough stopped trapping people, and stopped losing the Next buttonJul 29, 2026The dashboard/billing loop, properly this time, and every feature actually explainedJul 29, 2026Next moved forward and then jumped straight back a stepJul 29, 2026The walkthrough no longer holds people hostage, and the real cause of its flakinessJul 29, 2026Every long wait explains itself, not just the tailor pageJul 29, 2026The walkthrough's popover was covering the control it told people to pressJul 28, 2026Resumes keep their formatting - tailoring now edits bullets in place, and downloads are Word filesJul 28, 2026Project sections keep their structure, hyperlinks survive, and downloads use your own resume as the designJul 28, 2026Admin analytics were reading zero, and resume improvements now reach existing users without a re-uploadJul 28, 2026PDF download failed with ENOENT on every attemptJul 21, 2026Password reset, plus a fix for the "I signed up but it says wrong password" bugJul 20, 2026Vantage now learns which of your applications actually get repliesJul 18, 2026Sentry no longer records errors thrown by in-app browser scriptsJul 18, 2026Vantage now has a Discord communityJul 18, 2026Feedback is easier to reach at the moment it mattersJul 18, 2026Every error box across the app now has a "Report this bug" linkJul 17, 2026Chrome extension is live on the Chrome Web StoreJul 17, 2026Extension connect always failed with "Invalid token"; hydration errors on date labelsJul 17, 2026Auto-fill is extension-only - console snippet and answers panel removedJul 16, 2026Race-proof monthly quota countingJul 16, 2026Error monitoring with SentryJul 16, 2026Reorganized test suite with full unit coverageJul 16, 2026Codebase-wide comment auditJul 15, 2026Failed requests no longer count toward usage limitsJul 15, 2026Junk job parses (title "." and nothing else) are rejected instead of savedJul 15, 2026Gemini spend hardening - platform-wide AI budget and pro strategy-feedback capJul 15, 2026Tighter pro daily caps to keep worst-case AI spend under the subscription priceJul 15, 2026Example endpoint no longer calls the AI; Resume Studio pro cap 15/day; platform AI budget raised to ~$15Jul 15, 2026Privacy Policy expanded with data-collection and rights detailJul 15, 2026Pro pricing displayed as CAD instead of USDJul 15, 2026Loading status while your resume auto-fills your profileJul 15, 2026Removed the self-serve refund featureJul 15, 2026Removed the 7-day free trialJul 15, 2026Clearer login error when the email is not verifiedJul 14, 2026Resume Studio in the sidebar, tighter dev limit, louder feedback promptJul 14, 2026Feedback prompt resurfaces weekly; sidebar footer pinned to the bottomJul 14, 2026Tailor + ATS full-resume mode, tailoring-focused Resume Studio, hyperlink-preserving PDFs, rate-limit hardeningJul 14, 2026Tailor + ATS results tabs no longer send you backwards to run the next actionJul 14, 2026Help chat knowledge base caught up with today's featuresJul 13, 2026Rewrite generated outreach messages with a custom instructionJul 13, 2026Google sign-in, help chat, product emails, data reset, Resume Studio, and a full audit passJul 13, 2026Legal pages rewritten to match the current product, plus a Data Compliance pageJul 12, 2026Auto-apply question answering, account-switch stale data, ATS average skew, trial-status webhookJul 12, 2026Job pickers source from the application tracker (networking, ATS)Jul 12, 2026Substantive resume tailoring, personalized strategy feedback, identity-first outreach, strict contact role filterJul 12, 2026Full tailored resume hidden everywhere - users see only the per-bullet diffJul 12, 2026Manual outreach logging, full application editing, tailor from the trackerJul 12, 2026Job finder - saved named filters, km-radius search, salary/date filters wired to AdzunaJul 12, 20267-day free trial and 7-day self-serve refund on ProJul 12, 2026Mobile and tablet responsiveness pass across the appJul 12, 2026Second-round fixes - tracker-only listings, mobile sidebar, AI capacity errors, job feed persistenceJul 12, 2026Third-round fixes - scored_at bug, desktop-only gate, working salary/date filters, usage limits pageJul 12, 2026Mobile notice leak, filter cleanup, honest Adzuna label, connection requests rewrittenJul 11, 2026Daily tier-aware rate limiting across every AI and external endpoint; Groq references retiredJul 11, 2026Three-tier rate limiting (dev/free-monthly/pro-daily), free-tier 150 application capJul 11, 2026Admin account bypasses all per-user rate limits for testingJul 10, 2026Dark-glass visual overhaul - gold removed from buttons/cards/badges/inputs, kept only as a restrained accentJul 10, 2026Landing hero rebuilt as an overlay composition - metallic headline in front of a dimmed workspace with a studio-light beamJul 10, 2026Hero mockup unified into one app screen with the headline bottom-leftJul 10, 2026Hero lighting reworked to match the reference - diagonal shaft from the header, dark cornersJul 10, 2026Landing cards get a metallic-glass treatment - light diffuses inward from the borderJul 10, 2026Bespoke feature artifacts and a premium closing CTAJul 10, 2026Landing page containers widened to the hero mockup's lineJul 10, 2026Hero lighting tuned, metallic gold CTA button, Pro card elevatedJul 10, 2026Landing polish - features header animation, macOS window dots, real autofill data, level pricing cards, CTA detailsJul 10, 2026Closing CTA becomes live metal - cursor-tracked lightingJul 10, 2026Cursor light on all cards and mockups; closing CTA composition upgradedJul 10, 2026Metallic-glass rollout across the dashboard, resources, company, and legal pagesJul 10, 2026Light-mode hero slash goes gold; Pro pricing card differentiated in light modeJul 10, 2026Dark mode is the default; Pro card becomes a dark island in light mode; docs/blog cursor light removedJul 10, 2026Feedback page, changelog timeline, custom scrollbars, external-link icon, wider reading pages, gilded Pro cardJul 10, 2026Contact merged into feedback, signup agreements, changelog jump list, Pro card gold line, footer linksJul 10, 2026Glass sidebar highlight, connection-code copy cleanup, tailor toggle arrowJul 10, 2026Sidebar highlight keeps its gold bar and tint with a glass fill; extension toast uses the real logoJul 10, 2026Extension popup restyled to the dark-glass system; SEO hardeningJul 10, 2026Google Search Console verification tag wired to an env varJul 8, 2026Plain-language pass across all user-facing copyJul 8, 2026Blog post - The New Vantage design overhaulJul 8, 2026Subtle gold particle background on the landing pageJul 8, 2026Unified page letterhead across every dashboard pageJul 8, 2026One answer-method control per interview session instead of per cardJul 8, 2026Auto-apply page now leads with Chrome extension setupJul 8, 2026Documentation accuracy pass across all docs pagesJul 8, 2026Stop showing raw errors to users; log them to console.error insteadJul 8, 2026Logo now legible in light modeJul 8, 2026Light mode is now the default themeJul 7, 2026Design system v2 - Outfit/Manrope fonts, gold accent, elevation tokensJul 7, 2026Sidebar nav hover stateJul 7, 2026Landing page redesign - gold signature treatment, Outfit/Manrope type systemJul 7, 2026Landing page density + gold visibility passJul 7, 2026Landing hero eyebrow re-specced, pain-point cards raisedJul 7, 2026Dashboard design overhaul - Outfit/Manrope type, gold signature, new Auto-apply pageJul 7, 2026Landing page depth pass - feature hierarchy, section headlines, quote cards, gold glowJul 7, 2026Closing CTA section as a hero bookendJul 7, 2026Closing CTA rebuilt as a glowing band with a before/after diff artifactJul 7, 2026Consistent skeleton loading states across all dashboard pagesJul 7, 2026Dashboard consistency pass - custom selects, gold primary buttons, unified badges/cards/labelsJul 7, 2026Job pickers everywhere now source exclusively from the application trackerJul 7, 2026Full audit - everything that consumes jobs sources from the application trackerJul 7, 2026CustomSelect component replaces every native dashboard dropdownJul 7, 2026Dashboard polish pass - input focus glow, zebra rows, rich empty states, danger zone, interview mid-sectionJul 7, 2026Full SEO pass - metadata, sitemap, robots, structured dataJul 6, 2026Chrome extension - version marker for stale-build detectionJul 6, 2026Chrome extension - prompt option clicks now register, closest-match selection, websites slot discoveryJul 6, 2026Chrome extension - full-name skill queries, single-click prompt selection, degree listbox supportJul 6, 2026Tailoring results as a per-bullet diff view; PDF flow removedJul 6, 2026Tailoring - active JD keyword weaving, per-entry grouping in the diff view, tighter cardsJul 6, 2026Reverted shadcn init - restored original design systemJul 4, 2026Chrome extension - Workday multi-entry auto-add fixed; fill logic now verified by a DOM harnessJul 4, 2026Chrome extension - Workday Add-button pointer clicks, restricted skills early abort, LinkedIn/Websites dedupeJul 4, 2026Chrome extension - Workday label-based field discovery for tenants without automation-ids, section DOM dumpsJul 4, 2026Chrome extension - Workday spinner dates via keydown, multiselect school prompts, partial skill queries, websites fill-onlyJul 4, 2026Codebase-wide - replaced em/en/long dashes with regular hyphensJul 4, 2026Interview prep - tracker job dropdown was always emptyJul 4, 2026Chrome extension - Workday spinner overwrite, multiselect activation, websites manual-add noticeJul 4, 2026Chrome extension - Workday dates via ARIA arrow-stepping, prompt search committed with EnterJul 2, 2026Chrome extension - kit unwrap bug caused 0-field fills (Workday and all platforms)Jul 2, 2026Chrome extension - Workday stale-element fills, skills tag selection, website slot mapping, full fill diagnosticsJul 1, 2026Chrome extension auto-fill overhauled - two-tier fill, zero hallucination policyJun 9, 2026Cerebras model updated to gpt-oss-120bMay 25, 2026PDF printBackground, spacing, and tailor tech stack preservationMay 25, 2026Profile form - generic placeholders and month picker date inputsMay 25, 2026Networking - inline delete confirmation, remove AI-generated contact fallbackMay 25, 2026Networking - Apollo People Search replaces Jina/AI contact scrapingMay 22, 2026parse-profile extracts work experience and projects from resumeMay 22, 2026Experience section on profile page - editable cards with inline saveMay 22, 2026Projects section on profile page - editable cards with tag-based tech stackMay 22, 2026Experience and projects injected into all AI promptsMay 22, 2026PDF generation - no browser headers, HTML-preserved formatting, 1-page layoutMay 22, 2026Cover letter - proper letter format + personal template supportMay 22, 2026Puppeteer PDF generation - pixel-perfect server-side resume PDF via ChromiumMay 22, 2026Surgical tailor prompt + professional PDF CSS templateMay 22, 2026Tailor metrics preservation + PDF bullet rendering + hyperlink retryMay 22, 2026PDF spacing, bullet disc, metrics preservation, keyword stuffing, project link injectionMay 18, 2026AI-powered form fill via CerebrasMay 18, 2026.docx resume upload support + graduation year range fixMay 18, 2026Extension - phone dropdown filter, proximity panel detection, and open-ended question fillMay 17, 2026Strategy report - data-driven job search insightsMay 17, 2026Job feed "Tailor resume" pre-fill + secondary Groq key for job scoringMay 17, 2026Stripe billing - checkout, webhook, customer portal, and billing pageMay 17, 2026Networking - contact discovery, outreach message generation, and trackerMay 17, 2026Networking contact search - URL encoding, logging, and AI fallbackMay 17, 2026Cerebras AI for interview prep + full UI restyleMay 17, 2026Chrome Extension for job application auto-fillMay 17, 2026Extension - custom div-based dropdown handler for Greenhouse and LeverMay 17, 2026Extension - fix dropdown bleeding and Lever location selectMay 16, 2026Auto pre-fill profile from uploaded resumeMay 16, 2026Resume upload embedded in profile form with onboarding bannerMay 16, 2026Application detail modal with ATS scores and tailored documentsMay 16, 2026Status dropdown renders as portal to fix overflow clipping in tracker tableMay 16, 2026Settings panel with account, appearance, resume, and danger zone sectionsMay 16, 2026Documents page with filter tabs, expandable rows, copy and downloadMay 16, 2026Answer application form questions with AIMay 16, 2026Apply Prep page — application kit, question answerer, submit checklistMay 16, 2026Job Feed — Adzuna-powered personalized job discovery with AI relevance scoringMay 15, 2026App landing page at / with full marketing sectionsMay 15, 2026Shared marketing layout with nav, footer, and dark/light mode toggleMay 15, 2026Launch 1 - ATS-first job search workspaceMay 15, 2026Blog pages with MDX rendering, search, and category filteringMay 15, 2026Admin analytics dashboardMay 15, 2026Contact page with public form and rate-limited API route
Jul 30, 2026
Improvement

The walkthrough now has you use every feature, and catches it when one fails

  • •it walked through the whole app but only ever waited on the resume upload and the tailoring. Everything else - applications, the job feed, networking, interview prep, Resume Studio - was a sentence pointing at a heading. Those five now ask you to actually do the thing and wait for you, so a finished walkthrough means a used app rather than a read one
  • •every one of those waits has a failure path. A step that waits on work which can fail with nowhere to report the failure is a trap: you did what it asked, it broke, and the popover still says do it. lib/onboarding.ts now carries failOn/failBody for the resume upload, the tailoring itself, logging an application, the job feed, networking and interview prep, matching what analysing a job already had
  • •new markers behind the state they represent, so nothing can advance on a click that failed: application-logged, jobs-listed, jobs-no-roles, networking-result, studio-loaded, plus resume-error, tailor-error, log-error, jobs-error, networking-error, interview-error, studio-error (components/ResumeUpload.tsx, components/ResumeStudio.tsx, tailor/tracker/jobs/networking/interview pages)
  • •Strategy and Auto-apply stay explanations on purpose, and a test keeps them that way: Strategy needs 15 logged applications before it produces anything and Auto-apply needs the extension installed, so demanding either would strand a new account on a step it cannot finish
  • •a waiting step now says "Skip this bit" instead of "Next". The only visible way past one used to be Skip tour, which ends the whole walkthrough - "I don't want to do this one" and "stop showing me this" are not the same thing
  • •a step whose work is already done explains instead of waiting. Replaying it, you already have a resume and applications, so the wait resolved instantly and the step flashed past unread - the sections you had used were the ones it never told you about. Caught twice: before the step renders, and again inside 1.5s of it, because these pages fetch their own data and a slow load arriving looks exactly like you having just done it
  • •fix: reloading the page could throw away your place and restart from the beginning. Number(null) is 0, so a missing saved position read as a legitimate "step 0". Resuming sets the active flag but only pressing Next writes the position, so coming back to step 6 and reloading sent you to step 1
  • •fix: the Resume Studio step was silently skipped for anyone who had used it before. It anchored to the source picker, which does not exist once the Studio restores the document it keeps in your browser, so the step timed out and moved on. It now anchors to either state and reads correctly in both
  • •fix: the Strategy step opened "Strategy needs history first" and closed with "come back once you have a few weeks in" to people looking straight at a finished report. It describes the feature and states the 15-application threshold as a condition instead
  • •lib/appKnowledge.ts: the help chat was still describing a walkthrough that "never generates anything"
  • •tests: 4 new. The anchor test now checks advanceOn and failOn as well as target - a typo in either waits forever, and a failOn that matches nothing turns a failure back into that same forever wait
Jul 30, 2026
Fix

The walkthrough said tailoring logs the application for you

  • •it does not. Tailoring on Tailor + ATS ends with a Log button filled in from the posting, and it only runs when you press it - checked against the source, not the wording: the only two inserts into applications are that button and the tracker's own form, and neither runs on its own
  • •this one mattered more than a wrong sentence. Told that applications are tracked for them, someone stops logging any, and the tracker quietly stops reflecting their search - which then feeds "What is working", Strategy, and every AI feature that reads the history
  • •lib/appKnowledge.ts said "when logging by hand", which implied some other automatic path, and the Tailor + ATS entry never mentioned the Log button at all. Both now state that nothing is tracked unless you press Log
  • •tests: 1 new, asserting the guarantee against the source and scanning every step body, fail body and the help-chat knowledge for the claim
Jul 30, 2026
Update

The walkthrough explains again instead of making you do things

  • •the hands-on version is reverted. It had each page's step block until it detected you had done the thing, and two problems came with that which were structural rather than tunable
  • •HALF THE ANCHORS DID NOT EXIST YET. A step pointing at the tailored-resume panel points at markup that only exists after a job has been analysed and a resume tailored. Skip that work and the tour spent four seconds hunting each missing anchor, invisibly, then gave up on it - and every give-up could push a route. Pressing "skip this bit" on the Analyze step left the popover gone but the tour still walking the rest of /tailor, so trying to go anywhere else dragged you back until the chain ran out, then dumped you on Applications
  • •a walkthrough is also not something you can fail. Making someone produce output before they may read the next sentence turns an explanation into a gate, and a gate in front of a third-party lookup that can fail is a dead end
  • •every target is now required to be present on a freshly loaded page, with nothing typed and nothing generated. That single rule is what keeps the tour off the route-pushing path, and a test enforces it by name for the four anchors that caused it
  • •20 steps, still covering every section. The /tailor step describes the flow in words rather than trying to walk screens that do not exist yet
  • •fix: steps eased in rather than cutting. Each step now plays a short entrance animation (globals.css). Done in CSS rather than with driver's own animate option, which was tried first: it does tween, and it also leaves its transition state machine half-finished - the previous target keeps its highlight class so two elements stay lit, and its fade strands the popover at partial opacity whenever it restarts mid-run, which is the faint unreadable popover it was disabled for originally. A CSS entrance cannot go stale, settles opaque with forwards, and respects prefers-reduced-motion
  • •components/OnboardingTour.tsx: the outcome watcher, the failure re-wording and the "Skip this bit" relabel are gone with the waits. lib/appKnowledge.ts updated - the help chat was describing a walkthrough that asks you to do things
  • •the data-tour markers added for the waits are removed from the six pages that carried them
  • •tests: the waiting suite is replaced by one that asserts the tour asks for nothing, points only at load-time anchors, still reaches every section, and eases in
Jul 30, 2026
Update

Pro is counted per month like Free, and no longer sold as unlimited

  • •Pro was rate limited per day while Free was per month, so the two tiers could not be compared and the pricing pages papered over it with "unlimited". They were not: every Pro feature had a daily wall behind it, and for four of them - contact searches, job feed refreshes, resume uploads and profile imports - the daily cap was 1, so a Pro user could not do two resume uploads in a day where a Free user could do three back to back
  • •every Pro rate limit is now counted over a month, on the same window as Free. The numbers are the old daily caps x30, rounded up, so nobody loses anything in the switch: tailoring 150/month, ATS checks 250, cover letters 120, job parses 300, auto-fills 300, interview sessions 150, answer feedback 300, outreach 250, help chat 300, Studio edits 500, PDF downloads 2000, and 30 each for the four that were 1/day
  • •strategy feedback had no real limit at all. It was flagged proUnlimited, so its stated 5/day was dead code and ?force=true could regenerate it without bound. It is 30/month now - the report reads your whole application history, which barely moves day to day, so a fresh one roughly daily is the useful ceiling rather than an arbitrary one
  • •the 429 told Pro users to "try again tomorrow", which on a monthly window would have sent them back to the same wall the next morning. Free and Pro now both say next month; only the Free message mentions upgrading
  • •the pricing tables on the landing page, the billing docs, the README and CLAUDE.md carry the real numbers instead of "Unlimited". Application tracking is the one row that still says it, because it is the one feature with no rate limit behind it and genuinely has no Pro cap - that is now stated rather than implied. lib/appKnowledge.ts too, so the help chat stops promising unlimited use
  • •/limits already read its window from the tier, and now reports "per month" for Pro rather than "per day"
  • •tests/static/pro-monthly-window.test.ts (new): the window is a literal in each of the twenty route configs rather than in one shared place, so a new route can reintroduce the daily one just by copying its neighbour. Every config is read out of the source and checked, and the check is verified to fail - by flipping one route back to 1440 and watching it name the file - rather than trusted because it went green. It also refuses a Pro limit below the Free one, which is the invariant the day/month mismatch made impossible to state
  • •tests: 7 new
Jul 29, 2026
Fix

Downloaded resumes now keep their layout, links stay clickable, and the preview hides raw URLs

  • •lib/docx/slotFormat.ts (new), lib/docx/inject.ts: the real cause of "the formatting is bad, everything under the name is gathered up in one spot". Injection referenced the template's paragraph styles, but almost no real resume defines any - a .docx exported from Word declares Heading1-6, Title and ListParagraph and does everything else with direct formatting, because the author selected the text and pressed the buttons. Roughly half our slots therefore had nothing to map to and inherited Normal, so the name, contact line and every section heading collapsed into plain left-aligned body text. Unmapped slots now carry their own formatting - centred bold name, centred contact line, section headings with a rule, indented bullets with a glyph. A template that genuinely does define styles is still followed exactly and imposes nothing extra
  • •lib/docx/template.ts: suggestMapping no longer matches Word's built-in style names. Every .docx declares Heading1, Title and ListParagraph whether the author used them or not, so a hand-formatted resume was "finding" a style for half its slots - and Heading1 is 16pt blue Calibri Light, which looks nothing like the small bold heading the resume actually used. Matching only deliberate names ("Resume Bullet", "Contact Line") means a personal resume falls through to formatting designed to look like a resume
  • •lib/docx/resolveTemplate.ts: the style mapping is now read off the file on every download instead of trusting the copy stored at upload time, so improvements to the matching rules take effect for everyone immediately rather than only for templates uploaded afterwards
  • •lib/docx/pdf.ts: the PDF contact line had every link piled up in one spot. pdfkit's align: 'center' and continued: true do not compose - each run was centred against the full column independently rather than continuing from the last. Centring is now measured and positioned by hand. Verified by reading the link rectangles back out of the generated PDF: they previously overlapped (one link ended at x=433 while the next began at x=422) and now run strictly left to right
  • •lib/docx/links.ts, components/resume/Editable.tsx: the preview showed "LinkedIn (linkedin.com/in/jane)" because the address has to travel through the pipeline as text. It now shows just "LinkedIn" while reading and reveals the full text on focus, so the address is still editable but never shown as something the finished resume will contain. React no longer renders the field's text at all - it is written imperatively, because letting React reconcile a contentEditable node mid-edit collapses the caret to position zero
  • •lib/tagged/parseResume.ts: PUBLICATIONS had the same flattening as PROJECTS - a title line and its three bullets mashed into one entry joined by "|". The rule is now about shape rather than section name: one line per entry belongs in the named section, a title with bullets under it belongs in <custom> as an <entry>, whatever the heading says
  • •tests: 15 new tests - the formatting fallback across name, contact, headings and bullets, that a styled template still wins, that a bare Word file now maps nothing, PDF link rectangles never overlapping and staying inside the margins, and link collapsing never losing a non-link character
  • •no new DB columns, no new env vars, no packages
Jul 29, 2026
Fix

Section titles no longer render as bullets, and deduping stopped taking unique content with it

  • •lib/tagged/restructure.ts (new): PUBLICATIONS arrived as a single line with the title and all three bullets joined by pipes, so the title rendered with a bullet glyph in front of it, indistinguishable from the bullets belonging to it - the same fault PROJECTS had. A packed line is now split back into a title and its bullets deterministically, rather than relying on the parse prompt to produce the right shape. The split is by position, not length: the first segment is always the title, because a publication's name often runs longer than any of its bullets. Short trailing segments ("Co-Author", the year) rejoin the title, where the resume had them
  • •lib/tagged/restructure.ts: this runs for every stacked section - publications, awards, certifications - and for lines inside custom sections, so it is not a fix for one section that will need repeating for the next one. A section that is genuinely one line per entry is left exactly where it is; two or more sentence-length segments after the title is what marks a line as packed, because a title followed by a single long description is a normal one-line entry and splitting it would invent structure the resume never had
  • •lib/tagged/dedupe.ts: deduping now removes duplicated LINES rather than duplicated sections. The previous version dropped any section that was mostly a copy, which meant a section restating the education entry that also carried a "Relevant Coursework" line lost the coursework as well - unique content discarded for keeping company with duplicated content. That was the missing education bullet. Whatever was not already printed elsewhere now always survives, and a section disappears only when every one of its lines turned out to be a copy. The two overlap thresholds are gone with it; the rule is simply whether a line repeats something already said
  • •lib/docx/pdf.ts: added a page-fit check before every line and bullet. pdfkit paginates only for text it positions itself, and every line here is drawn at an explicit y so it can be centred or share a baseline with a bullet glyph - an explicit y is honoured even past the bottom margin. Nothing was observed overflowing in testing, but the manual y handling made it possible, and a line drawn below the page is still present in the file and extractable by a text parser, so it would have been invisible in exactly the way that is hardest to notice
  • •lib/tagged/baseResume.ts: pipeline version bumped to 3, so every existing resume re-derives with the unpacking and line-level deduping on the user's next tailoring rather than needing a re-upload
  • •tests: 16 new tests - unpacking a packed publication, leaving genuine one-liners alone, the same handling for awards and certifications, and the coursework case that regressed
  • •verified by rendering a full resume through the PDF and diffing every expected line against the extracted text, and by decompressing the content streams to confirm nothing is drawn below the bottom margin
  • •no new DB columns, no new env vars, no packages
Jul 29, 2026
Fix

Project and publication titles were being deleted, and a publication was promoted to a section of its own

  • •lib/tagged/dedupe.ts: the missing titles were my own regression from the previous change. Deduping treated a line as a repeat if anything already printed appeared ANYWHERE inside it, so a project titled "Vantage | Github | TypeScript, Next.js, React, Supabase | Jan. 2026" matched because "TypeScript" was also in the skills list. Every project whose tech stack the candidate had listed as a skill lost its title. A line is now a repeat only when most of it - three quarters - is made up of content already printed, which still catches a job collapsed onto one line, where essentially every word came from somewhere else
  • •lib/tagged/restructure.ts: the model sometimes emits a section NAMED AFTER one of its entries - a custom section headed "Artificial Intelligence and Machine Learning: A New Frontier" holding that paper's bullets, while <publications> keeps only the title stub. The title then rendered as a section heading in its own right. Such a heading is now recognised as an entry title (it carries pipes, or runs past forty characters - no real section heading does), turned back into the entry's label, and filed under the section it came from, with the leftover stub removed. Two split publications merge into one PUBLICATIONS section rather than becoming two
  • •lib/tagged/restructure.ts: titles are matched on their opening rather than by containment. The two copies of a split entry usually keep different tails - one ends "| Co-Author | Jun. 2024", the other stops at the name - so neither contains the other, but they always share the beginning
  • •lib/tagged/baseResume.ts: pipeline version bumped to 4, so every stored resume re-derives with these fixes on the next tailoring
  • •tests: 8 new tests - a project title surviving when it names a listed skill, a line genuinely made of already-printed content still being dropped, an orphaned publication being filed back under PUBLICATIONS with its full title, a real section heading being left alone, and two split publications merging into one section
  • •verified by running the exact shape from the reported screenshot through the pipeline: publications empties, the section is named PUBLICATIONS, and both the publication and project labels come through intact
  • •no new DB columns, no new env vars, no packages
Jul 29, 2026
Improvement

Restructuring rules generalised beyond the one resume they were written against

  • •the previous three changes were all written while looking at a single CS student's resume, which is how heuristics quietly end up encoding one document. Audited the constants introduced and fixed the three that were genuinely overfitted
  • •lib/tagged/restructure.ts: entry unpacking recognised the pipe and nothing else. A resume separating with middots, bullets or tabs got no restructuring at all. It now detects which separator a line uses, splits on that, and rejoins the title with the same one - a resume written with middots no longer comes back written with pipes. Dashes are deliberately excluded: "Jun. 2024 - Sep. 2024" is a date range, and splitting on it would cut every title line in half
  • •lib/tagged/restructure.ts: a segment was classed as a bullet purely on character count, which assumed verbose bullets. "Cut waste by a third in six weeks" says as much as a sentence twice its length. Word count now carries the terse case and length the wordy one
  • •lib/tagged/restructure.ts: the section-heading-versus-entry-title test was a forty-character cutoff, which put "LEADERSHIP AND COMMUNITY INVOLVEMENT" (36) and "PROFESSIONAL MEMBERSHIPS AND AFFILIATIONS" (41) on opposite sides of a boundary neither belongs near - the second was being demoted from a heading. Word count is the real discriminator: headings are names of one to five words, titles are phrases. Measured against realistic headings and titles, the two overlap at five words and nothing can separate them there, so the bar sits just above the headings. An ambiguous five-word title stays a heading, which keeps the section and all its content rather than burying a real one
  • •tests/lib/taggedGenerality.test.ts (new): 16 tests built from resumes deliberately unlike the one that prompted the work - a nurse, a chef, a doctoral researcher; middot, tab and pipe separators; terse bullets; headings sitting on the boundary; and resumes with no structure to recover. Two of them assert the property that actually matters, that restructuring only ever moves text and never removes a word
  • •no behaviour change for resumes that were already handled correctly; all previous tests pass unchanged
Jul 29, 2026
Improvement

Structure is now inferred from the resume rather than matched against a list of known formats

  • •the previous change replaced one hardcoded separator with a list of eight, which is the same mistake written longer: it works until it meets a resume using a slash or a guillemet, and then does nothing at all while looking like it should
  • •lib/tagged/restructure.ts: separators are detected by behaviour instead. A separator is punctuation that sits with whitespace on both sides and does so repeatedly - that is a property of the text, not a character to be enumerated. Verified against characters named nowhere in the codebase, including the triangle, the four-pointed star, the double slash and the guillemet. The whitespace requirement is also what stops the hyphen in "state-of-the-art" and the dot in "Node.js" being candidates, without either needing to be special-cased
  • •lib/tagged/restructure.ts: the title/bullet boundary within a line is now derived from that line. A packed entry holds two kinds of segment - short fields naming the thing and clauses describing it - and those groups sit far apart in length whatever the resume's house style, so the segments are sorted by word count and the widest proportional gap is the boundary. A fixed threshold has to choose between a verbose CV where the gap falls at fourteen words and a terse one where it falls at seven; measuring the gap fits both. Finding no clear gap is itself an answer: a line whose segments are all of a size is a title line, not a title plus bullets
  • •lib/tagged/restructure.ts: whether a heading is really an entry title is now judged against the headings that resume actually uses, including the built-in ones, rather than against a fixed word count. A CV whose sections are called "Professional Experience and Employment History" sets a different bar from one using "WORK"
  • •lib/tagged/restructure.ts: added casing as a signal, which is what finally separates "Wrap It Up | Github" from "AWARDS & HONOURS (2020-2024)". Both are two fragments around a symbol and no measurement of length or shape can tell them apart; what can is that resumes are typographically consistent about headings, so one that breaks the pattern is not a heading. Only consulted when at least three other headings agree, so a resume with mixed-case headings simply gets no signal here
  • •absolute numbers survive only as floors, and are documented as such: a segment under four words is never a bullet, and a heading under six words is never demoted. They rule out the impossible rather than defining the boundary, which is measured
  • •tests: the generality suite grew to 18, now including four separators that exist nowhere in the source, and a case showing a five-word title being caught by casing where word count could not
Jul 29, 2026
Fix

Empty title boxes in the preview, and a prompt rule against merging bullets

  • •lib/tagged/dedupe.ts: when an entry's title turned out to be a duplicate, its bullets were left behind in a titleless entry, which the preview rendered as an empty "Title, links, tech stack, date" placeholder with bullets hanging under it. An entry with no title is not an entry - it is a few loose lines - so that is what those bullets become now. This was visible on the EDUCATION section, whose title repeated the real education entry while the coursework line beneath it did not
  • •lib/tagged/parseResume.ts: added an explicit rule against merging two source lines into one tag, with the observed failure as a worked example - a publication whose three bullets arrived inside a single tag with no separator between them ("Research Paper Published research", "using ML Built a linear"). The rule is tied to a signal that is actually in the extracted text: a bullet glyph marks where a new item begins, so it must be used to split before it is stripped
  • •no deterministic repair for the merged case, deliberately. Once two sentences are inside one tag with nothing between them, the only way back is to guess where the boundary was, and the available signal - a capital letter mid-line - is also how every proper noun starts ("in Arseam Foundation", "students in Python"). A wrong split would corrupt correct text, which is worse than leaving a bullet long
  • •tests: 2 more, covering the education case exactly as it appeared and the general rule that orphaned bullets become loose lines
Jul 29, 2026
Improvement

Link detection now uses where text sits rather than a list of TLDs

  • •lib/docx/links.ts: the last enumeration in the pipeline is gone. Deciding whether "janedoe.dev" was an address used a list of accepted TLDs, plus a second list of technologies that collided with them ("socket.io", "asp.net"). Both lists had the same flaw: they only knew what someone had thought to write down, so an unusual domain went unlinked and the next framework named after a TLD would have become a broken link
  • •"Node.js" and "janedoe.dev" are the same shape, so nothing in the characters says which is an address. What says it is where it appears. A dotted word on the contact line is an address, because that line is a list of ways to reach someone; a dotted word in a bullet is a technology, because that is what bullets talk about. The generators all know which field they are rendering, so that context is now passed through
  • •everything else is recognised structurally and works anywhere in the document: a scheme, a "www.", an "@", or a path. A name does not have a path, which is why "github.com/jane/repo" links inside a bullet while "socket.io" does not
  • •patterns are Unicode-aware, so an internationalised domain is not the one that quietly fails
  • •tests: link coverage grew to 26, including TLDs no list would have contained (.quest, .zone, .engineering, a non-Latin one) and the collision cases now handled by position rather than by name
Jul 29, 2026
Fix

Corrupted dates in the PDF, and a publication losing its title

  • •lib/docx/pdf.ts: every tab-aligned date in the PDF came out mangled - "Jun. 2024" as "" un. 2024", "Nov. 2025" as ""aov. 2025". Resumes right-align their dates with a tab, so a project title arrives as "...OpenSCAD\tFeb. 2026", and pdfkit's built-in fonts have no glyph for a tab: it emitted a replacement glyph that also swallowed the character after it. Control characters are now turned into spaces before anything is drawn. It looked like an encoding fault somewhere far upstream and was neither - the .docx of the same resume was clean, which is what pointed at the renderer
  • •lib/tagged/restructure.ts: a publication lost its title entirely. When the model splits an entry, the two halves of its title line land in different places - the paper's name became the section heading while "Co-Author | Research Paper | Jun. 2024" stayed as the entry label - and the code kept the label and discarded the heading. The finished resume listed a publication with no name. The two are now merged, with neither allowed to replace the other, and a title already contained in the label is not repeated
  • •lib/docx/inject.ts: tabs in the Word output are emitted as <w:tab/> rather than left inside <w:t>, which is how Word actually records one
  • •lib/tagged/baseResume.ts: pipeline version bumped to 5
  • •tests: 9 new - the tabbed date across five month formats, ordinary punctuation surviving the same pass, the split-title merge, and a property check that no word of either half is lost
  • •found by reading the actual .docx and .pdf downloads rather than a screenshot: the .docx contained "Co-Author | Research Paper \tJun. 2024" intact, which located both faults in minutes
Jul 29, 2026
Fix

Education entries can carry their own lines, so coursework stops becoming a section of its own

  • •lib/tagged/schema.ts: ResumeEducation gained bullets. "Relevant Coursework" kept appearing as a section at the very end of the resume, after PROJECTS, and no heuristic was at fault - the schema had nowhere to put a line beneath a degree, so the model had to invent somewhere and invented a section. Scrubbed modelled education as one line because a recruiter's template prints it as one line; a student's resume is the opposite, with the degree on one line and everything distinctive about it hanging underneath
  • •this is the same shape of problem as the publications one, and the same kind of fix. Where the format cannot represent a resume the model improvises, and no amount of prompt instruction or downstream repair removes the need to improvise. Closing the gap does
  • •carried through every stage: the tag format nests <bullet> inside <edu>, the structure check counts them so tailoring cannot drop one, and Word, PDF, the plain-text rendering and the browser editor all print and edit them under the qualification
  • •lib/tagged/tailor.ts: education bullets are locked, not tailored. Coursework and a thesis title are facts, and rewording them is not tailoring
  • •lib/tagged/parseResume.ts: the prompt now shows <bullet> inside <edu> and says plainly that anything printed under a qualification belongs to it - coursework, a thesis, honours, a society - and is never its own section
  • •documents stored before the field existed are read as having no bullets, so nothing needed migrating. Pipeline version bumped to 6, so every resume re-derives on its next tailoring
  • •tests: 10 new, including that coursework prints between EDUCATION and PROJECTS rather than after both in all three output formats
Jul 29, 2026
Fix

A resume keeps its own section order instead of being rearranged into Vantage's

  • •SECTION_ORDER was a constant in the code and custom sections were appended after all of it, so every resume came back in Vantage's order rather than its own. A candidate who leads with PROJECTS had them moved to the end, and one who puts education above experience had that swapped. The styles were coming from the user's template, but the layout was not
  • •lib/tagged/sectionOrder.ts (new): the order is read off the source text rather than asked of the model. The extracted text still has the sections in the order they were printed, so where each one sat is a fact that can be measured. Each section is located by its heading first - matched loosely, so "WORK EXPERIENCE" and "Technical Skills" still match the plainer names we know them by - and by its own content when there is no heading to find
  • •a heading only counts when it sits alone on a short line, which is what stops "eight years of experience across payments" inside a summary from being mistaken for the EXPERIENCE heading
  • •lib/tagged/schema.ts: ResumeDoc gained order, and resolveSectionOrder turns it into a single list of sections to render. All four outputs - Word, PDF, plain text and the browser editor - now walk that one list instead of iterating SECTION_ORDER and then appending the custom sections, so none of them can disagree about the order and the editor shows what the download will produce
  • •anything the recorded order does not mention keeps its default position rather than being dropped or pushed to the end, so a section added in the editor after parsing still appears somewhere sensible, and a resume whose order could not be read renders exactly as it did before
  • •lib/tagged/tailor.ts: the order is locked like every other fact about the resume - tailoring rewrites bullets, it does not move sections
  • •documents stored without an order render in the default order, so nothing needed migrating. Pipeline version bumped to 7
  • •tests: 10 new, covering the derivation, the summary false-positive, differently worded headings, and the order actually reaching all three output formats
Jul 29, 2026
Fix

Content filed under the wrong adjacent heading is put back where the resume had it

  • •lib/tagged/reheading.ts (new): a publication came back under a "RELEVANT EXPERIENCE" heading, which read as though the resume had two experience sections. The two headings are adjacent in the source and the model attached the content to the second one
  • •this class of fault does not need a better prompt, because it is not a judgement call. The source says exactly which heading a piece of content sits under, so rather than asking more carefully the parser now checks: find where the content actually appears in the extracted text, find the nearest heading above it, and use that name. The resume is the authority on its own layout
  • •headings are recognised three ways, any one being enough: the line is short and capitalised the way this resume capitalises its headings, it matches a heading the document already claims to have, or it matches one of the standard names. Being generous is safe here because only the nearest heading above the content wins
  • •only the heading is changed. Content is never moved between sections, so the pass cannot lose or duplicate anything - at worst it leaves a heading alone for want of evidence
  • •runs before deduping and ordering, both of which reason about which section content is in and so need it filed correctly first
  • •pipeline version bumped to 8
  • •tests: 6 new, including the exact PUBLICATIONS / RELEVANT EXPERIENCE layout that produced the fault, a resume with title-case headings, and resumes where no heading can be identified at all
Jul 29, 2026
Fix

A resume keeps its own wording for its section headings

  • •the duplication was two faults compounding, and this was the one that made it visible. A resume heading its jobs "RELEVANT EXPERIENCE" had them printed under "EXPERIENCE", because the heading text came from DEFAULT_SECTIONS rather than from the document. Alongside a second section the model had mislabelled, that read on screen as two experience sections where the candidate had written one
  • •lib/tagged/schema.ts, lib/tagged/sectionOrder.ts: ResumeDoc gained headings, filled from the source at parse time by the same pass that reads section order - it was already finding each section's heading line, and was throwing the text away. Word, PDF, plain text and the editor all print the resume's wording and fall back to the standard name only when none was found
  • •this is the same principle as section order and section position: what a resume calls its sections, what order it puts them in, and which heading a piece of content sits under are all facts about the document. Reading them is more reliable than deciding them, and it removes one more thing the model can get wrong
  • •locked during tailoring, like the order - tailoring rewrites bullets, it does not rename sections
  • •pipeline version bumped to 9
  • •tests: 6 new, covering the heading being read, printed in all three formats, surviving the round trip, resisting a tailoring, and falling back to the standard name when the resume has no heading of its own
Jul 29, 2026
Fix

No two sections can print the same heading

  • •lib/tagged/headingCollisions.ts (new): the duplication was never duplicated content - it was two sections printing the same heading. The resume headed its jobs "RELEVANT EXPERIENCE", and once that wording was honoured (v9) the mislabelled section carrying the same name collided with it. Each heading was individually defensible, which is why correcting them one at a time never resolved it: the pair was the fault
  • •expressed as an invariant rather than a rule about any particular section, so nothing is keyed to what sections are called. The same check covers a repeated "RELEVANT EXPERIENCE", two "Board Positions" sections, or any pair a future resume produces, and comparison ignores case and spacing
  • •the duplicate loses its heading, never its content. Blanking it makes the content read as a continuation of the section above - which is what the model claimed by giving it that name - and cannot lose anything, where renaming would mean inventing a name and dropping would mean discarding whatever it held. A test asserts no word is ever lost
  • •built-in sections claim their heading first, because theirs is the resume's own wording for content the format understands; a custom section repeating it is the one that was mislabelled
  • •runs after headings are read and before the order is derived, since suppressing a heading changes a section's identity and therefore how the order refers to it
  • •pipeline version bumped to 10
  • •tests: 8 new, including the reported layout end to end, a collision between two sections the code has never heard of, and the guarantee that content survives regardless
Jul 29, 2026
Fix

Content printed inside a section now stays inside it, instead of becoming an unnamed section

  • •the previous change was wrong, and caused both faults reported after it. A resume lists things inside sections the format models separately - a publication under EXPERIENCE - and blanking the duplicate heading turned a merely mislabelled section into an unnamed one, floating at whatever position the order derivation guessed. The publication ended up printed under SKILLS
  • •lib/tagged/headingCollisions.ts: rewritten. The heading is evidence of where content belongs, so it is acted on rather than erased. A custom section whose heading matches a built-in section's is marked as part of it, and renders directly after that section's own content under the one heading - which is where the resume had it. Nothing is keyed to particular names: the rule is that a matching heading means "part of", whatever either is called, and it matches the resume's own wording rather than our default
  • •lib/tagged/schema.ts: CustomSection gained partOf, and resolveSectionOrder splices absorbed sections in after their parent rather than ordering them by where their content was found. Their placement is stated, not inferred
  • •lib/tagged/dedupe.ts: a duplicated skills section survived deduping and then became the second unnamed section. A list section prints as one comma-separated line, and the duplicate arrived as that whole line, but it was only ever compared against the individual skills - each too short to be distinctive. The joined form is now compared too
  • •absorbed sections are locked through tailoring, so a tailoring cannot re-parent content
  • •pipeline version bumped to 11
  • •tests: 5 new covering the real layout - publication under EXPERIENCE after the jobs, exactly one EXPERIENCE heading, nothing unnamed, a genuinely separate PROJECTS section untouched, and matching on the resume's own heading wording
Jul 29, 2026
Feature

The resume format now has no fixed sections - a resume's sections are whatever the candidate wrote

  • •every bug in this pipeline came from one design decision, and this removes it. The format had named buckets - experience[], education[], skills[], publications[], plus custom[] for the rest - so the AI had to route each piece of a resume into one of them. Content the buckets could not hold had to be improvised somewhere, and each improvisation needed a repair pass to detect and undo. A publication printed inside EXPERIENCE became a second experience section; coursework under a degree became a section at the end; a section the resume called "RELEVANT EXPERIENCE" was relabelled "EXPERIENCE"
  • •a resume is now a name, a contact line, and an ordered list of sections. Each section has the heading the candidate wrote, and holds a paragraph, titled entries with bullets, standalone lines, or any combination. There is no wrong bucket to put something in because there are no buckets, and no list of known sections because the resume's sections are the sections
  • •deleted outright: lib/tagged/dedupe.ts, restructure.ts, reheading.ts, headingCollisions.ts, sectionOrder.ts, and every heuristic in them - duplicate detection, packed-entry unpacking, orphan-title reattachment, heading correction, collision suppression, section-order derivation, heading derivation. None of it has anything to do any more. Around 900 lines of repair logic replaced by a format that does not need repairing
  • •lockFields is now one rule for the whole document - bullets may change, nothing else may - where it previously needed a clause per section type and a new section type meant remembering to add another
  • •the Word style slots dropped from seventeen to nine. The old list had one per section type, which is exactly the assumption being removed: a bullet is a bullet whichever section it is in
  • •documents stored in the old shape are converted rather than rejected, so a resume tailored before the change can still be downloaded and nobody is told to start again
  • •pipeline version bumped to 12, so every stored resume re-derives on its next tailoring
  • •tests: the suite shrank with the code it covered. tests/lib/realResume.test.ts is new and asserts the reported resume end to end - the section keeping the name "RELEVANT EXPERIENCE", the publication printing inside it after the jobs, skills appearing once, coursework staying under education, every heading appearing exactly once, no unnamed section, and the same order reaching Word and PDF
Jul 29, 2026
Fix

A resume saved by an older build no longer crashes the editor

  • •Resume Studio keeps the working document in sessionStorage, so a resume left there before the format changed came back with no sections at all and took the whole page down on first render
  • •lib/tagged/validate.ts: readStoredDoc is now the single entry point for a document that has been sitting somewhere - a database row, sessionStorage, a request body. Current shape passes straight through, the old bucketed shape is converted, anything else is refused. Used by the Studio, the tailor page, the base-resume loader and the Studio download route, so no path trusts stored data to match today's code
  • •lib/tagged/schema.ts: normaliseDoc fills in a document's missing fields, and the editor runs everything through it before rendering. Stored data outlives the code that wrote it, and a missing array should be a degraded render rather than a crash
  • •tests: 5 new covering conversion, pass-through, refusal of anything else, and normalising a document with no sections or a section with no fields
Jul 29, 2026
Fix

Dates print against the right margin, and a transcription that drops a bullet is caught and retried

  • •two faults reported together, both about the document saying less or looking less like the original than it should
  • •right-aligned dates. Resumes put dates hard against the right margin, and the tab that does it survives extraction - so the tab is the instruction, and honouring it reproduces the layout exactly. That is better than the alternative of guessing which part of a line is a date, which would work until it met a line the guess did not fit
  • •lib/docx/links.ts: splitAtTab splits a line at its last tab. The last one, because a line with several is laying out columns and the right margin is where the final one lands
  • •lib/docx/slotFormat.ts, lib/docx/inject.ts: a tabbed line gets a right tab stop at the margin and emits a real <w:tab/> element rather than a tab character, which Word ignores inside <w:t>. Lines without a tab are untouched, so nothing that was already correct moves
  • •lib/docx/fallback.ts: the same in the built-in layout, via the docx library's right tab stop
  • •lib/docx/pdf.ts: the tail is drawn separately, right-aligned on the same baseline. Two fixes here - the PDF has no tab stops to align to, and the baseline was two points low because the height being subtracted excluded the line gap that the draw had actually advanced by. Small enough to look like nothing on screen and like every date sitting low once printed
  • •missing bullets. Verified first that nothing in our own code was losing them: a full-size resume through serialize, parse and render kept all 23. The loss was the model dropping a line while transcribing, which is the worst kind of fault because nothing looks wrong - the document parses, renders and downloads perfectly, it just says less than the candidate wrote
  • •lib/tagged/completeness.ts (new): the source text is right there, so this is checkable rather than something to hope for. Every substantial line of the original is looked for in the parsed document, matched on a fingerprint of its opening so a bullet that legitimately lost its glyph and spacing still counts. Lines too short to be distinctive are skipped, since they appear inside other lines by chance
  • •lib/tagged/parseResume.ts: up to three attempts, and the bar is now "the result contains the resume", not "it parsed". A transcription below 90% coverage is rejected and retried with the dropped lines quoted back verbatim - showing the model the specific text it lost works where repeating the rule does not. The fullest attempt is kept and returned if none clears the bar, with a Sentry warning: a resume missing one line beats no resume at all
  • •90% rather than 100% because a resume's page furniture and the odd line the extractor garbles are legitimately absent, and failing every upload over one of those would be worse than the problem
  • •pipeline version bumped to 13, so a resume parsed before the completeness check re-derives on its next tailoring. Those documents may be missing a bullet and nothing about them looks wrong, so they would never be re-parsed otherwise
  • •tests: 23 new. The PDF ones read coordinates back out of the content stream, since extracted text cannot tell a right-aligned date from one sitting mid-line - they assert the date starts past the middle of the page, shares a baseline with the rest of its line, and leaves the following line where it belongs. The rest cover splitAtTab, tab stops in both Word paths, and the completeness check finding the dropped bullet, naming it, retrying, and giving up gracefully
Jul 29, 2026
Feature

A Word resume is no longer rebuilt - it is edited, so the download is your own document

  • •two bugs were reported together, a missing skills line and a missing education line, plus dates that had moved off the line they belonged on. Both came from the same place, and it was the same mistake as the section buckets one level further down
  • •the format still had fixed slots inside a section: one text field, and entries with a title, a subtitle and a meta. A resume that prints "Employer | Role | Location<TAB>Dates" on ONE line had to be split across three of them, so the date was torn off its line and printed underneath. A skills section printed as TWO paragraphs had one field to land in, so the second was silently dropped. Neither was a bug in the code that could be fixed; the shape had nowhere to put the content
  • •a section is now the lines the resume printed, in order, each marked as a bullet or not. There is nothing to route and nothing to split, so there is no wrong slot to end up in and no content that cannot fit
  • •and for a Word upload nothing is transcribed at all. lib/docx/paragraphs.ts (new) reads word/document.xml directly: the file already states every line, its order, its tabs and which paragraphs are bullets. All of that is read. The AI is asked one question - which lines are section headings - and every number it returns is checked against the file before it is believed. No content passes through a model, so no content can be lost, reworded or moved. The reported missing lines are now impossible rather than merely detected
  • •the download is the user's own file. lib/docx/rewrite.ts (new) reopens the uploaded .docx and changes only the paragraphs whose words changed. Every other paragraph is re-emitted byte for byte. Fonts, spacing, tab stops, bold spans, list numbering, borders, headers, footers, images and hyperlinks all survive because no code recreates them - the previous engine emptied the body and rebuilt it from our own idea of what a resume looks like, which is exactly the thing that should not exist
  • •verified against the reported resume: 44 of 44 lines present including both skills paragraphs and the coursework line, the publication still inside RELEVANT EXPERIENCE, all 10 tabs intact, and 47 of 48 paragraphs byte-identical to the original with only the tailored bullet differing
  • •lib/docx/extractText.ts: tabs are no longer collapsed into spaces. That single character is how a resume right-aligns its dates, and folding it away was what left the date with nothing to say it belonged on that line
  • •lib/tagged/serialize.ts: tabs cross the wire as an escape so trimming cannot take the layout with them, and the escape is reversible - a property test caught text containing a literal backslash-t decoding into a real tab, which would have invented layout out of a file path
  • •lib/tagged/completeness.ts: the line-length floor was 30 characters of fingerprint, which exempted exactly the lines that were going missing - a skills line fingerprints to 26. Now 14. The acceptance bar went from 90% to every line: a resume was previously allowed to lose three lines and pass. It still returns the fullest attempt when none is perfect, so this costs at most an extra call and cannot fail an upload
  • •the PDF path keeps the transcription and the retries, because a PDF has no structure to read. That difference is now explicit in the code rather than incidental
  • •Word style slots dropped from nine to five. Each drop came from removing an assumption about structure, not from cutting anything: a bullet is a bullet whichever section it is in, and a line is a line whether it names an employer or lists a language
  • •pipeline version bumped to 14, so every stored resume re-derives on its next tailoring and picks up its paragraph references. Documents stored in either older shape are converted rather than rejected, keeping every word
  • •tests: 410 passing, 51 new. tests/lib/ownDocument.test.ts builds a real .docx in the reported shape and asserts the whole guarantee - every line read, the entry kept whole with its tab, both skills paragraphs, bullet-ness read from the file, nothing changed when nothing was tailored, exactly one paragraph changed when one bullet was, list numbering surviving a rewrite, and every other part of the archive repacked untouched
Jul 29, 2026
Fix

Tailored downloads keep the link to the user's own document

  • •found while checking that the rewrite reached the tailor flow and not only a fresh upload. It did not
  • •tailorTagged took the tagged STRING and re-parsed it to get the document it restores locked fields from. The tagged format carries text only - a line's paragraph reference is deliberately never shown to the model - so that re-parse produced a document with no references at all, and lockFields dutifully restored nothing. Every tailored resume therefore arrived at the download with its link to the user's file already broken, and fell back to being rebuilt
  • •nothing about that looks wrong from the outside, which is the point: the document is complete, correctly tailored and readable. It is just formatted by us instead of by them, silently, on exactly the path that matters most
  • •lib/tagged/tailor.ts: tailorTagged now takes the ResumeDoc and serialises it itself, so the original it restores from is the real document rather than a lossy round trip of it
  • •tests: 2 new in tests/lib/ownDocument.test.ts running the route's own sequence - serialise, model answer, skeleton check, lock, clamp - and asserting the references survive it, then that the finished .docx changes every bullet and nothing else, with the tabbed entry lines byte-identical to the original
Jul 29, 2026
Fix

Resume Studio downloads the user's own file too, the way tailoring does

  • •reported as "tailor is perfect, Studio is a bit off, and the dates come out tabbed rather than right-aligned". Three separate causes, all of them Studio taking a different route to the same download
  • •lib/tagged/edit.ts: the edit prompt still listed the OLD tag vocabulary - job, company, title, dates, edu, school, skill, item - none of which exists any more. taggedToDoc rejects unknown tags, so every Studio AI edit failed both attempts and silently returned the document unchanged. Rewritten for the current seven tags, including keeping a printed line whole and leaving a tab where it is
  • •lib/tagged/sources.ts (new): an edit is allowed to restructure, so paragraph references cannot be restored positionally the way tailoring restores them. They are matched by text instead: a line whose wording came back unchanged is the same line and gets its paragraph back, and a line the instruction actually rewrote does not. Verified on the reported resume - 37 of 38 lines keep their reference through an edit, the one that does not being the bullet the edit reworded
  • •app/api/resume-studio/route.ts: a Word upload in the Studio is now read from its own paragraphs rather than transcribed, the same as everywhere else, so it arrives with references already attached
  • •lib/docx/rewrite.ts: the writer walked the FILE's paragraph order, which meant "move Projects above Experience" would have been silently ignored. It walks the document's order now, and each line still emits its own paragraph, so moving a line carries its formatting with it. Blank paragraphs travel with the line below them, so a moved section takes its spacing and a deleted one does not leave a hole
  • •lib/docx/rewrite.ts: sourcesFit (new) refuses to write a document into a file it did not come from. The references are plain indices, so pointing them at the wrong file does not fail - it writes each line into whatever paragraph sits at that number. Studio can open a file that is not the base resume, and a stored document outlives the upload it came from, so this is reachable. Judged on lines rather than bullets, since bullets are expected to differ after tailoring
  • •and nothing is right-aligned on the user's behalf any more. Their document decides: rewriting their file adds no tab stops at all, and the reported resume comes back with the same 10 tabs and zero stops of ours. The right-aligned stop applies only where Vantage supplies the layout - the built-in Word template and the PDF
  • •lib/docx/slotFormat.ts: where a stop IS written, its position is read from that document's own page size and margins. It was hardcoded to 6.5 inches, which on a page with half-inch margins lands an inch and a half short of the edge - looking exactly like a date tabbed across rather than aligned
  • •lib/docx/inject.ts: a tabbed line gets its stop whether or not the slot has a template style. The styled branch skipped it, on the reasoning that a template's style says everything - but a paragraph style rarely defines a tab stop, and one that does not leaves the date with nothing to aim at
  • •components/ui/DismissibleNote.tsx (new): the Word-versus-PDF explanation on both the Tailor and Studio pages can now be dismissed, and stays dismissed. Useful the first time, noise on the fortieth download. Reads localStorage through useSyncExternalStore rather than copying it into state in an effect, so the server and client renders agree and dismissing updates every copy of the note at once
  • •tests: 421 passing, 9 new covering a section moved in the editor keeping its order and its formatting, an edit reconnecting untouched lines while leaving the reworded one to be rebuilt, two lines never claiming one paragraph, and the writer refusing a document whose lines do not match the file
Jul 29, 2026
Improvement

Undo, redo and Save in Resume Studio, and a prompt to upload the Word version of your resume

  • •components/resume/useDocHistory.ts (new): undo and redo over the whole document. A step is one committed change rather than one keystroke - fields commit on blur, and the AI edit and the structural buttons each replace the document once - so the history reads the way a user would describe their own edits. Capped at 60 steps
  • •Save is real, not decorative. The editor already kept a draft in sessionStorage, which is per-tab and lost when the tab closes, so a Save button backed by that would have promised more than it delivered. Explicit saves go to localStorage and survive closing the tab, and the label says which of "Unsaved changes" and "Saved to this browser" is actually true rather than always claiming one
  • •the saved copy is read through useSyncExternalStore rather than mirrored into React state, so there is one source of truth for whether there are unsaved changes, and undoing back to the saved state correctly reports none
  • •Ctrl+Z, Ctrl+Shift+Z and Ctrl+S. Undo is ignored while the caret is inside a field, where the browser's own text undo is the right behaviour - undoing the whole document mid-word is not
  • •components/WordResumeNudge.tsx (new): tells someone whose resume is a PDF that uploading the Word version keeps their own formatting. Shown as a card on the dashboard, where it is worth interrupting for, and as an inline note beside the download buttons. Dismissing either dismisses both, because it is one piece of advice
  • •shown only to people it applies to. Whether the base resume is a Word file is answered on the server on the dashboard, so it never flashes at the users it does not apply to, and someone with no resume at all is left alone - the page is already telling them to upload one
  • •lib/docx/fileType.ts (new): isDocxFile moved out of extractText.ts, which pulls in a zip library the browser has no business loading
  • •components/ui/DismissibleNote.tsx (new): the Word-versus-PDF explanation on the Tailor and Studio pages can now be dismissed and stays dismissed. Useful the first time, noise on the fortieth download
Jul 29, 2026
Fix

Loading spinners were invisible in light mode

  • •the shared Spinner drew itself in var(--accent), which is the page's text colour - near-black in light mode. Most spinners sit inside the primary button, which is dark-filled with white text in both themes, so in light mode it was a near-black arc on a near-black button
  • •it inherits currentColor now, which is right everywhere without a per-place override: white on the dark button, dark on a light page
  • •also stopped injecting a duplicate @keyframes spin for every spinner on the page; it is defined once in globals.css
Jul 29, 2026
Fix

Page titles are consistent across every page in the app

  • •three different things were wrong at once. Some pages appended " - Vantage" to a title the root layout already appends " · Vantage" to, so Usage Limits read "Usage limits - Vantage · Vantage". Some had no title at all and fell back to the full marketing tagline, so /jobs read "Vantage - Your unfair advantage in job searching". And every docs page had a double-encoded middle dot, rendering a literal "·" in the tab
  • •every route now resolves to its own name plus " · Vantage", and the names match what the sidebar calls each page rather than being invented separately
  • •eleven client pages could not export metadata at all, which is why they had none: a client component cannot. Each has a small layout beside it that supplies the title and nothing else
  • •added to the four server pages that were missing one: Billing, Strategy, Contact, Admin
  • •app/(marketing)/layout.tsx declared its OWN template: '%s - Vantage', which overrode the root layout's '%s · Vantage' for every marketing page - Blog, Changelog, About, Feedback, the docs and the legal pages all rendered a hyphen while the app rendered a middle dot. Removed, so the site has one template rather than two that drifted apart. Its default was identical, so nothing was lost
  • •the first audit missed that, because it read page titles and assumed the root template applied. It now resolves the whole chain - the title from the page or the layout beside it, the template from the nearest ancestor layout that declares one - and the result is checked against the built HTML rather than the source
  • •app/not-found.tsx had no title either, so the 404 was the last page still showing the full marketing tagline
  • •all 45 routes verified, plus every prerendered page in the build output
  • •docs pages keep their "· Docs" qualifier deliberately: without it /docs/billing and /billing would both be "Billing · Vantage", which is ambiguous in search results and browser history
Jul 29, 2026
Fix

The upload-your-Word-resume card fills the width it is given

  • •the text carried a max-width meant to keep a comfortable reading measure, but the card is as wide as the dashboard - so the words wrapped in a narrow column with two thirds of a white box empty beside them
  • •the buttons moved out of the body and became its sibling, so they sit at the right of a wide card and fill it. Below 860px they wrap underneath instead
  • •and it had no bottom margin, so the stats row sat flush against it
Jul 29, 2026
Feature

Long waits say what is happening instead of spinning

  • •a spinner says "something is happening" for about five seconds. After that it stops saying that and starts looking like something has hung, because a spinner that has turned for thirty seconds is indistinguishable from one that will turn forever
  • •components/ui/SlowProgress.tsx (new): past a threshold the spinner is joined by the step the server is actually on, and a bar showing roughly how much of the wait is left. Under the threshold nothing appears, because an operation that was about to finish does not need a progress bar
  • •the step labels are accurate. Each list mirrors the awaits in its own route handler, in order: tailoring reads your resume, rewrites your bullets, then scores the result against ATS screening, because that is what app/api/tailor-resume/route.ts does. A Word upload in the Studio gets a different list from a PDF, because a Word file is read and a PDF has to be transcribed
  • •the bar is a time estimate, not progress the server reports, and the UI is built so it cannot pretend otherwise. No percentage is ever shown, since a number invites the reader to treat it as measured. The bar stops short of the end and pulses rather than filling, because a progress bar that reaches 100% while the request is still running is the one thing it must never do. Once it overruns its own estimate it says so
  • •wired into the tailor, cover letter and job-parsing actions, and into the Studio's upload, AI edit and download
  • •lib/debugSlow.ts (new): localStorage.setItem('vantage-slow', '25000') makes the client hold every tracked response for that long, so the long-wait UI can be seen on demand rather than by clicking buttons hoping to catch a slow request. The request really was made and really did return, so the UI goes through exactly the states it would on a slow day. Capped at two minutes, and inert unless set
  • •tests: 16 new. The component is a pure function of elapsed time, so fake timers cover all of it - that it stays hidden while a spinner is still convincing, names the right step at each point, advances, never fills while running, admits an overrun, never prints a percentage, and starts from zero on the next request rather than resuming
  • •also removed a ref in useDocHistory that was assigned during render and never read
Jul 29, 2026
Feature

A guided walkthrough of the app on first sign-in

  • •new users landed on a dashboard with thirteen sections and no explanation of any of them. The walkthrough now runs once on first arrival and points out each one in turn
  • •it costs nothing to take. No step runs an action, so no tailoring, job parsing or cover letter credits are spent by walking through it - and the first thing it says is that, because a new user has every reason to assume otherwise. Enforced by tests that read app/api/onboarding/route.ts and fail if checkLimit, consumeLimit, recordRateLimitUse or any AI call ever appears in it, and that assert the tour makes no request other than saving its own position
  • •nothing that failed is counted. A step whose target is not on the page - Billing behind the freemium flag, a section that did not render - is dropped before the tour starts, so "Step 6 of 14" always refers to fourteen steps that can actually be shown. And if saving progress fails, the walkthrough is not recorded as finished: the server keeps what it last knew and the user gets it again, because showing it twice is a small annoyance where losing it silently is a broken feature
  • •leaving is cheap by design. Position is saved after every step, so closing the tab at step four returns to step four rather than the beginning. Skip is on every step and is permanent. Long tours are known to lose people, and the mitigation chosen here is to make stopping and resuming free rather than to cut the tour short
  • •driver.js for the overlay: MIT, zero dependencies, framework-agnostic. The two better-known alternatives were both disqualified - react-joyride does not support React 19, and Shepherd.js and Intro.js are AGPL unless separately licensed, which is not a licence to take on for a commercial product
  • •lib/onboarding.ts holds the steps and the stored shape, so the client and the route cannot disagree about how many steps there are. A test checks the tour against the sidebar itself, so adding a section to the app without adding it to the walkthrough fails the build
  • •sql/06_onboarding.sql (new): profiles.onboarding jsonb. Kept on the profile rather than in the browser so the walkthrough follows the user to a new device, and so completion is measurable. Malformed or out-of-range stored values degrade to "not started" rather than throwing, because this renders on the dashboard
  • •Settings gained a Replay the walkthrough control, and /dashboard?tour=1 forces it without resetting anything
  • •tests: 22 new
Jul 29, 2026
Fix

The walkthrough now has you use the app, and three bugs in it

  • •it was a narrated tour of page headers. It visited each page and described it, which is not onboarding - it is a slideshow with navigation. Steps can now WAIT: advanceOn names something that only appears once the user has actually done the thing, and the tour sits there until it does. By the end they have uploaded a resume and tailored it to a real job rather than having read about both
  • •the three waits are on real state, not on clicks: the upload confirmation that only renders once a resume is stored, the parsed-job card that only renders once a posting came back, and the tailored-result panel that only renders once there is a tailored resume. So a click that failed cannot advance the tour
  • •the tour still never presses anything itself, so it cannot spend a use on the user's behalf. The one step that does cost a use says so before they press it, because finding that out afterwards would be a nasty surprise on a new account. Every waiting step also offers "Skip this bit", so nobody is trapped by an action they do not want to take
  • •order changed to match how the app is actually used: profile and resume first, then a real job tailored end to end, then everything else - which now has something to be about
  • •fixed a redirect loop after finishing. A Next layout does not re-render on client-side navigation, so run and the stored step stayed at their first-render values for the rest of the session. After the last step the tour read that stale position and drove itself back to Billing, then to the dashboard, then to Billing, forever. The tab now keeps its own final word on being finished, checked before anything else
  • •fixed the Skip button hitbox for the second time, properly. The label is now written into the element in JS. Both CSS attempts put the visible word and the clickable box in different places - all: unset stripped the button's box model, and font-size:0 with a ::after left the text overflowing below its own box, so it only responded to a click above the label
  • •the resume design upload on the Profile page is hidden. It predates this pipeline and is now redundant for anyone with a Word resume, and worse than redundant: resolveTemplate prefers it, which skips the rewrite and rebuilds the document instead. Nothing behind it was touched - the component, the route and any stored template still work exactly as before, so nobody who has one is affected. Only the way in is closed
  • •also fixed: adding the onboarding column to the dashboard's profile query meant that before sql/06 was run the whole select failed, so the greeting lost the user's name and fell back to their email prefix. It is its own query now, and an optional feature cannot take the page down with it
  • •tests: 11 new, covering the waits, the marker sources, the paid step warning it, and the loop fix
Jul 29, 2026
Fix

Replay the walkthrough did nothing

  • •it reset the stored progress, navigated to the dashboard, and then sat there. Two causes, both the same underlying fact: a Next layout does not re-render on client-side navigation
  • •the reset cleared the saved position but not the "finished in this tab" flag, so the tour stopped itself on arrival. And Settings had rendered while the walkthrough was already finished, so the layout it navigated through still passed run=false. Fixing either alone would not have been enough
  • •lib/onboarding.ts now owns the four session keys and a requestWalkthrough() helper. Three files have to agree about those keys and they were being spelled out separately in two of them, which is how they came apart. A test asserts Settings no longer writes them itself
  • •a forced replay starts from the beginning rather than the saved position, because someone pressing Replay wants the whole thing, and the request is consumed on use so it runs once rather than reopening on every page
  • •the database reset still happens, so the replay survives a full reload as well
  • •tests: 6 new
Jul 29, 2026
Fix

The walkthrough now follows the tailor page's actual flow

  • •the steps had been written from the shape of the code rather than from walking the page, and it showed. Tailoring is three screens - paste and Analyze, then Continue off the job preview card, then the actions - and the walkthrough jumped from Analyze straight to "press Tailor My Resume", which is on the next screen and not rendered yet. There was no step for Continue at all
  • •worse, the signal it waited on for "a job was parsed" was attached to the collapsed job header, which only renders on screen three. So the wait could never be satisfied where the tour was standing
  • •and it did move anyway, for the wrong reason: overlayClickBehavior was 'nextStep', so any stray click on the backdrop advanced the tour past whatever it was waiting for. It is a no-op now. Next, Back and Skip are the only ways to move
  • •the tailor steps are now paste and Analyze, check what it read and press Continue, press Tailor My Resume, then the result - with the waits attached to markers that appear on the screen the step is actually standing on. A test asserts that order against the page
  • •a failed job analysis is handled. Previously the step waited forever on a success that was never coming, still telling the user to paste a job link they had just pasted. A step can now name a failure signal too: when it appears the step stays put and re-words itself to explain what happened and offer the paste-the-description route instead. It keeps waiting afterwards, so a retry still moves them on
  • •the actions step also mentions Check ATS Score and Generate Cover Letter, which sit next to the button it points at
  • •tests: 4 new
Jul 29, 2026
Fix

The walkthrough stopped trapping people, and stopped losing the Next button

  • •the overlay was swallowing every click outside the highlight. That is what made the failed-job-analysis message useless: it told the user to switch to pasting the description, and the toggle that does it sits outside the highlighted input, so the one thing they were being told to press was the one thing they could not. The overlay is pointer-events: none now, so the whole page stays usable - which a walkthrough that asks the user to do real work needs anyway
  • •the Next button read "Skip this bit" on steps that were not waiting for anything. The label was set on the driver instance, but a same-page step change reused that instance, so every step after the first on a page inherited the first one's configuration - its button text and its padding. The step index is React state now, so each step builds its own driver and cannot inherit anything. The button reads "Next" throughout; the close control reads "Skip tour"
  • •lib: the waiting logic moved into watchStepOutcome, which is exported and tested against a real DOM. Scanning the source for a string does not answer "does it recover from a failed job analysis" - that is a behaviour. 12 new tests cover advancing, re-wording on failure, continuing to wait so a retry still counts, not spinning while the error is still on screen, preferring a success that arrives alongside a stale error, and ignoring a failure signal a step never asked about
  • •the starting step is resolved at first render rather than in an effect, which also removed a setState-in-effect the linter was right about. The component renders nothing in either case, so there is no hydration mismatch to cause
  • •tests: 13 new
Jul 29, 2026
Fix

The dashboard/billing loop, properly this time, and every feature actually explained

  • •the loop was /billing redirecting. That page sends you to the dashboard whenever ENABLE_FREEMIUM is not 'true', which is the default. The tour pushed /billing, the server bounced it to /dashboard, the tour read the same step and pushed /billing again. It never reached the end, so the "finished in this tab" flag added earlier could not help - it was guarding a loop that happens after completion, and this one happens before it. That fix was aimed at a cause I had assumed rather than found
  • •fixed at both levels. The Billing step is now marked freemiumOnly and is not in the list at all when paid plans are off, so the tour never walks to a page that will not hold still. And generally: if a step's route is pushed and the pathname ends up somewhere else, that step is stepped over instead of retried, so any future redirecting page degrades to a skipped step rather than an infinite bounce
  • •seven features were being "explained" by highlighting their page heading. Tracker, Jobs, Strategy, Networking, Interview Prep, Auto-apply and Resume Studio now each get a step pointing at the control that does the work, saying what it does and what it needs: the Log application button and what the statuses are for, refreshing the feed and how a saved job carries into tailoring with its description attached, the message generator and the three message types, starting a practice session from a tracked job, connecting the extension and that it fills but never submits, and what the Studio's two editing modes are
  • •Strategy says it needs 15 logged applications, because it refuses below that. Presenting it as ready to use was setting people up to click it and get an error
  • •tests: 19 new. Every feature page is asserted to have a step pointing at something other than its heading, and to say more than a restatement of its title - so "onboarding" that only highlights a header fails the build. Plus the Billing gating both ways and the general bounce guard
Jul 29, 2026
Fix

Next moved forward and then jumped straight back a step

  • •goTo wrote the new position to the tab but only updated the component's own index on the same-page branch. So after navigating, the index still pointed at the step on the page just left - and the effect that keeps the tour on its step's page dutifully pushed back to it. Forward, then back, every time a step crossed to another page
  • •the index now moves first, whether or not a navigation follows
  • •the give-up guard was rebuilt while fixing it. It counted attempts, which meant a slow navigation looked identical to a page refusing to load, and a slow connection would skip real steps. It is time-based now, and when a page does refuse it gives up on every step that lives there rather than retrying the same dead route once per step
  • •tests: 6 new, and these ones render the component with the router mocked instead of scanning the source. Both bugs in this entry were invisible to a source scan and obvious the moment the tour is actually rendered and told which page it is on: that it navigates forward, does not push back to the page it just left, drives a step belonging to the page it landed on, remembers the position across the navigation, steps over a page that never loads, and waits out a slow one
  • •also replaced the last source-scanning assertion about the bounce guard with the behavioural one, having watched it break for the third time over a renamed variable
Jul 29, 2026
Fix

The walkthrough no longer holds people hostage, and the real cause of its flakiness

  • •it was trapping people. Pressing Next on a step that was waiting, then trying to visit any other page, pushed you straight back - the effect that keeps the tour on its step's page had no idea whether a navigation was its own doing or the user's. A walkthrough is a suggestion, not a cage: it now recognises a navigation it did not start, goes quiet, and keeps its place so returning to that page picks it up again. Verified in a browser: clicking Applications mid-tour lands on Applications, and going back to Profile resumes at the same step
  • •found the cause of most of the erratic behaviour, and it was not any of the individual symptoms. driver.js highlights an element by MUTATING it - adding a class, aria-haspopup, aria-expanded, aria-controls, and marking the parent. Those are nodes React owns. The tour lives in the layout, and a layout's effects run before the page segment beneath it has hydrated, so driver was reaching into not-yet-hydrated DOM. React then hydrated, found attributes it had not written, reported a mismatch and threw the subtree away - taking the element the tour was pointing at with it. That is why steps showed on one page and not another, why popovers vanished, and why behaviour changed on reload. The tour now waits for the page to go idle before it highlights anything
  • •the popover was rendering at a fraction of full opacity. driver fades the popover and the overlay with a CSS animation, and the popover is rebuilt on every step, so the animation restarted before finishing and the whole thing sat faint and unreadable behind the page. Measured it at opacity 0.22 in the browser. The fade is off now
  • •the overlay no longer swallows clicks, which is what made the failed-job-analysis message useless: it told the user to switch to pasting the description, and that toggle is outside the highlight
  • •the /billing loop was gated on the wrong flag. /billing redirects on ENABLE_FREEMIUM, the server flag, but the walkthrough was gated on NEXT_PUBLIC_ENABLE_FREEMIUM - and on this project the two disagree, so the tour still walked to a page that bounced it back. It reads the server flag now
  • •a hard ceiling on navigations per session, as a backstop. Every loop so far had a different cause, so this one is for whatever the next cause turns out to be
  • •added a GET to /api/onboarding. "Why is the walkthrough not running" was otherwise unanswerable from the browser, because the state only ever reached the client as a prop - a stale row looked identical to a broken component
Jul 29, 2026
Improvement

Every long wait explains itself, not just the tailor page

  • •the progress loader was added to tailoring, job parsing and cover letters, and the other eight long operations were left with a bare spinner
  • •now covered: reading an uploaded resume, the ATS check on both the Tailor and ATS pages, the strategy report, generating an outreach message, generating interview questions, assessing an answer, and reading a posting from the tracker's detail modal
  • •each has its own steps, named after what its route actually does in the order it does it - reading a resume is upload, then read it line by line, then fill in your profile; a strategy report is read your history, then look for what is working
  • •tests/static/slow-progress-coverage.test.ts (new): every AI-backed route is checked to have a caller, and every caller is checked to show progress. Missing seven of eight is exactly the gap nobody notices until a user is staring at a spinner, so it is checked rather than remembered. It also rejects a one-step stage list or a vague label, since either would pass a coverage check while telling the user nothing
  • •writing that test found two call sites I had missed on my own list - the ATS page and the tracker's detail modal - and one bug in the test itself, which only recognised single-quoted 'use client' and so silently skipped the interview page
Jul 29, 2026
Fix

The walkthrough's popover was covering the control it told people to press

  • •found by driving the walkthrough in a browser rather than reasoning about it. On the step that asks for a job, the failure message says to switch to pasting the description instead - and the popover sat directly on top of that toggle. elementFromPoint over the toggle returned the popover's own description element, so the one control the message pointed at was the one control that could not be clicked
  • •driver places its popover under whatever it highlights, and the highlight was the input row alone. The anchor is now the whole "give us a job" group - the input, the toggle, the textarea and the error - so the popover sits below all of it
  • •verified after the change: the popover no longer overlaps the toggle, and the toggle is the actual hit target at its own centre point
Jul 28, 2026
Feature

Resumes keep their formatting - tailoring now edits bullets in place, and downloads are Word files

  • •the old pipeline asked the AI to return the whole resume rewritten as plain text, then tried to rebuild the formatting afterwards from an AI-generated HTML copy. Formatting, hyperlinks and page layout were lost at the first step and approximated at every step after. The resume is now a structured document end to end, and the AI is only ever allowed to edit the text inside it
  • •lib/tagged/schema.ts, serialize.ts, skeleton.ts (new): the tagged resume format - a closed tag vocabulary (name, contact, summary, job, company, title, dates, bullet, education, skills, and a custom-section escape hatch so PROJECTS, VOLUNTEERING or anything else you wrote survives under its own heading). tagSkeleton() fingerprints the tag sequence with all text stripped out, which is how structural drift becomes detectable rather than something you hope the prompt prevented
  • •lib/tagged/parseResume.ts (new): raw resume text to a structured document. Validated by round-tripping through our own parser, not by trusting the model - one retry with the parse error fed back
  • •lib/tagged/tailor.ts (new): tailoring, with three checks between the model and your resume. assertSameSkeleton rejects any change to the number of jobs, the number of bullets, or the section order. lockFields restores every field that is not a bullet or summary, so a company name or graduation date cannot drift. clampBulletLengths reverts a bullet that grew more than ~15%, which is the one-page guard - the template's margins are untouched, so page count only moves if a bullet wraps. Two failed attempts return the resume unchanged rather than something broken, and that case does not consume a tailoring
  • •lib/tagged/tailor.ts: the per-bullet diff and the skill-gap list are now computed from the two documents instead of asked for. The old prompt had the model explain its own edits ("Added PostgreSQL keyword") and report its own gaps, neither of which anything checked. Reasons are now true by construction
  • •lib/tagged/edit.ts (new): Resume Studio's instruction pass. Deliberately weaker than tailoring - it allows structural change, because "move Projects above Experience" is the whole point there - but still guarantees the result parses with no invented tags
  • •lib/docx/extractText.ts (new, replaces lib/extractResumeText.ts): DOCX extraction now goes through mammoth's HTML conversion rather than raw text, and PDF extraction through pdf-parse v2 with link annotations enabled. Both write hyperlink destinations into the text, so "LinkedIn" no longer arrives with its address thrown away
  • •lib/docx/inject.ts (new): injects the document into your own .docx, replacing only the contents of <w:body>. styles.xml, header, footer, images and the trailing <w:sectPr> (page size, margins, header reference) are carried across untouched. Extended beyond the reference implementation to emit real <w:hyperlink> elements with matching entries in word/_rels/document.xml.rels, allocated above the template's highest existing rId
  • •lib/docx/links.ts (new): finds the addresses hiding in a field's text and splits it into plain and linked runs. Bare domains are matched against a TLD allowlist plus a small denylist, so "Node.js", "React.ts" and "ASP.NET" do not become blue underlined links in the middle of a bullet
  • •lib/docx/fallback.ts, template.ts (new): the clean default layout for users without a template, and template storage (Supabase Storage, private resumes bucket) plus the style-slot matching that runs unattended on upload
  • •app/api/resume-template/route.ts (new): upload, check and remove your Word template. Styles are parsed before the file is stored, so an unreadable file is rejected up front rather than failing later at download time
  • •app/api/tailor-resume/docx/route.ts, app/api/resume-studio/docx/route.ts (new): the download endpoints. No AI, so re-downloading after every edit is free
  • •app/(dashboard)/tailor/page.tsx: "Full resume" is now a live editor - click any line to change it - and the button is Download Word. The document is edited, not the text, so no edit can produce something that will not render
  • •components/ResumeStudio.tsx: rebuilt on the same document. Both editing paths are available - click a line to change it, or describe a bigger change. Seeding from a tailored resume is now instant (nothing to rebuild)
  • •components/ResumeTemplateUpload.tsx (new): the Word template control on the Profile page
  • •removed: lib/resumeHtml.ts, lib/generatePdf.ts, lib/resumeCss.ts, lib/extractResumeText.ts, /api/tailor-resume/render, /api/resume-studio/pdf. HTML is gone from the resume pipeline entirely, and with it the headless-Chromium PDF renderer, the fuzzy bullet-text matching, and up to two AI calls per resume upload
  • •profiles.resume_html is kept in the database for existing rows but is no longer read or written. Resumes uploaded before this change have no structured document yet; the first tailoring creates one and saves it, so nobody needs to re-upload
  • •new DB columns: resumes.tagged_doc (jsonb), documents.tailored_doc (jsonb), profiles.resume_template_path (text), profiles.resume_template_name (text), profiles.resume_template_mapping (jsonb). Migration in sql/04_tagged_resume_pipeline.sql, which also adds the storage UPDATE policy that replacing a template needs
  • •packages: added jszip and docx, upgraded pdf-parse to v2, removed puppeteer-core and @sparticuz/chromium-min. next.config.ts marks pdf-parse and mammoth as server-external packages
  • •tests: 50 new tests across tests/lib/tagged.test.ts, taggedTailor.test.ts, docxLinks.test.ts, docxInject.test.ts and docxRoundTrip.test.ts - property-based round-tripping of the format, the structural and field-lock guards, relationship-id allocation against a real .docx, and a full generate-then-re-read round trip proving a hyperlink survives both directions
  • •no new env vars
Jul 28, 2026
Fix

Project sections keep their structure, hyperlinks survive, and downloads use your own resume as the design

  • •lib/tagged/schema.ts: custom sections gained entries - a titled thing with its own bullets, via new <entry> and <label> tags. A PROJECTS section used to flatten into one undifferentiated bullet list, so "Wrap It Up | Github | TypeScript Jan. 2026" rendered as though it were another of its own bullets. The title line now has somewhere to live. Flat sections (Interests, Coursework) still use bare <item>, and normaliseCustom() fills in entries for documents stored before it existed, so nothing needed migrating
  • •lib/tagged/tailor.ts: because a project title is now separate from its bullets, project bullets can be tailored while the title stays locked. lockFields restores every <label> verbatim, clampBulletLengths holds project bullets to the original length, and diffBullets reports them under their own section heading instead of changing them silently. findSkillGaps searches title lines too, since that is where the tech stack is named
  • •lib/tagged/dedupe.ts (new): drops a custom section that restates a built-in one. On a resume whose heading reads "RELEVANT EXPERIENCE" rather than the bare word "EXPERIENCE", the model was filling <experience> AND adding a custom section with the same roles, so every job printed twice - as did education. The match is on the text rather than the heading, so "Volunteer Experience" holding genuinely different roles survives, and a "Publications" section on a resume with nothing in <publications> is kept rather than losing the only copy
  • •lib/tagged/baseResume.ts (new): fixes hyperlinks vanishing from downloads. Resumes uploaded before this pipeline were being tagged from their stored raw_text, which the old extractor produced by discarding every hyperlink target - so "LinkedIn" arrived as dead text before tagging even started. The original file is now fetched back out of storage and re-read with the current extractor, and the better text is saved over raw_text while we are there (buildUserContext reads that column and was missing the same links)
  • •lib/docx/resolveTemplate.ts (new): downloads now use your uploaded resume as the design when it is a Word file. The resume you already use IS the design you already like, so your fonts, margins, header and spacing come through with nothing to configure. An explicitly uploaded template still wins; a PDF resume falls through to the Vantage layout
  • •lib/docx/pdf.ts (new): PDF download, alongside Word. This is NOT a conversion - rendering the .docx would need LibreOffice or Word, neither of which runs on a serverless function - so the PDF is drawn from the same ResumeDoc with the same section order and the same link handling. Stated plainly in the UI: Word carries your design, PDF is a clean standard layout. Both carry working links
  • •lib/docx/pdf.ts: pdfkit silently drops a link annotation when x/y are passed as explicit undefined on a continued run, which produced blue underlined text that did nothing when clicked. Continued runs now use the two-argument form; a test asserts the /Link and /URI objects are actually in the file
  • •app/globals.css, app/(dashboard)/resume-studio/page.tsx: the Studio preview was squeezed into about 540px - the 900px page cap minus a 340px sidebar - so every line wrapped in the wrong place. Two-column workspaces now use a wider container
  • •lib/tagged/parseResume.ts: prompt gained a worked example for project entries, an explicit rule against emitting the same content twice, and a rule about copying "Label (address)" pairs whole
  • •components/resume/ResumePreview.tsx: titled entries render and edit as a title with bullets under it, with separate "+ titled entry" and "+ line" controls
  • •tests: 21 new tests across tests/lib/tagged.test.ts, taggedDedupe.test.ts and resumePdf.test.ts - entry round-tripping, the skeleton still catching a dropped entry, dedupe keeping what it should, and PDF link annotations
  • •packages: added pdfkit and @types/pdfkit. No new DB columns, no new env vars
Jul 28, 2026
Fix

Admin analytics were reading zero, and resume improvements now reach existing users without a re-upload

  • •app/admin/page.tsx: user metrics have never worked. The page selects profiles.created_at, and that column has never existed - profiles only had updated_at - so the query errored, the result was discarded unchecked, and total users, new this week, activation rate, both retention cohorts and the growth table all rendered as 0 or "-" no matter how many people signed up. sql/05 adds the column and backfills each user's REAL signup time from auth.users, so the history is recovered rather than everyone being stamped as joining today
  • •app/admin/page.tsx: failed queries are now named in a banner at the top instead of degrading silently to zero. This is the reason the missing column went unnoticed for so long - a broken query and a genuinely quiet week looked identical. events, route_logs, costs and subscriptions are still not defined in sql/ at all, so if any of them has drifted too it will now say so
  • •app/admin/page.tsx: the Refresh button was a next/link to the route you are already on, which resolves from the router cache and can leave every number unchanged - "stale data" that was really "nothing refetched". It is a plain anchor now. Unbounded selects also gained explicit limits, since PostgREST silently caps at 1000 rows
  • •lib/tagged/baseResume.ts, sql/05: resumes now carry a tagged_version. When the parsing pipeline improves, every stored document is stale, and previously the only fix was asking each user to re-upload. A version below the current one is now treated exactly like a missing document: the original file is re-read and re-parsed on the user's next tailoring. Existing rows are stamped 0, so everyone picks up project entries, section dedupe and hyperlink recovery automatically
  • •lib/tagged/dedupe.ts: duplicate detection no longer keys off a list of built-in section names. The underlying failure is the model hedging about where content belongs, which it can do for any section, so a custom section is now checked against the built-in sections, against every custom section already kept, and against itself for repeated lines. The heading only chooses the threshold - 60% overlap when it names a section we model, 90% when it does not - so a Projects section sharing a phrase with a job survives while a near-total restatement is dropped
  • •components/ResumeUpload.tsx, app/(dashboard)/tailor/page.tsx, components/ResumeStudio.tsx: added the note that uploading Word is what makes tailored downloads keep your own fonts and layout, and that the PDF uses a standard layout regardless. Also in the docs and the help chat, with a new FAQ entry for "my downloaded resume does not look like my own resume"
  • •tests: 6 new dedupe tests covering custom-vs-custom duplication, repeats inside a section and inside a titled entry, and the two overlap thresholds
  • •new DB columns: profiles.created_at (backfilled from auth.users), resumes.tagged_version. Migration in sql/05_admin_created_at_and_tagged_version.sql. No new env vars, no packages
Jul 28, 2026
Fix

PDF download failed with ENOENT on every attempt

  • •lib/docx/pdf.ts: pdfkit's default build reads its standard-font metrics (Helvetica.afm and friends) off disk at runtime from a path derived from __dirname. The bundler rewrites that path - under Next it resolved to "C:\ROOT...\node_modules\pdfkit\js\data\Helvetica.afm", which does not exist - so constructing a PDFDocument threw ENOENT and every PDF download 500'd. Switched to pdfkit's standalone build, which inlines those metrics behind a virtual filesystem, so there is no disk read left to get wrong in dev or on a serverless function
  • •types/pdfkit-standalone.d.ts (new): the standalone build ships without declarations, so it borrows the ones from pdfkit's normal entry point - same API, different font source
  • •the unit tests passed throughout: vitest does not bundle the module the way Next does, so it never reproduced the fault. Verified instead against a real production build (next build + next start), and confirmed the cause by reverting the import and watching the identical ENOENT come back
Jul 21, 2026
Fix

Password reset, plus a fix for the "I signed up but it says wrong password" bug

  • •app/(auth)/signup/page.tsx: signing up with an email that already has an account no longer silently sends you to onboarding for an account you can't log into. Supabase returns a fake user with an empty identities array in that case (to prevent email enumeration) and never sets a password - we now detect that and explain the email is taken, pointing Google users to "Continue with Google". This was the root of a report where a Google user tried an email/password signup with the same address and then got "incorrect password" on login
  • •app/(auth)/forgot-password/page.tsx (new): request a password reset link by email. Always shows the same confirmation regardless of whether the email has an account, so it can't be used to probe for registered emails
  • •app/(auth)/reset-password/page.tsx (new): where the reset link lands - set a new password (session already established by the callback). Shows a "link expired" state with a re-request button when there is no recovery session. Works for Google-only accounts too: setting a password adds a password identity so they gain a second way to sign in
  • •app/auth/callback/route.ts: honours a same-origin ?next= param (validated against open redirects) so the reset link can reuse the existing PKCE code exchange, then forward to /reset-password. Also now reports OAuth code-exchange failures to Sentry instead of silently redirecting to /login?error=oauth - "Continue with Google didn't work" reports were previously undebuggable
  • •components/GoogleSignInButton.tsx: the swallowed signInWithOAuth error is now captured to Sentry
  • •app/(auth)/login/page.tsx: added a "Forgot password?" link; the wrong-password message now suggests Google sign-in and password reset
  • •lib/appKnowledge.ts: help chat now knows about password reset and the Google-vs-email-account distinction
  • •no new DB columns, no new env vars, no packages
Jul 20, 2026
Feature

Vantage now learns which of your applications actually get replies

  • •lib/outcomes.ts (new): the outcome loop - joins each application to the resume sent with it and the ATS score that resume earned, then compares response rates across those attributes. Purely statistical, no AI call, so the numbers shown to users and injected into prompts are derived from their data rather than inferred by a model. Only counts applications with a final outcome (interviewing, offer, rejected, ghosted); still-pending "applied" rows are excluded so they cannot be read as failures. Needs 15 decided applications before reporting anything, at least 5 on each side of a comparison, and a 1.4x difference before a factor is called a signal
  • •lib/userContext.ts: outcome analysis added as a seventh parallel fetch and a new "WHAT HAS ACTUALLY WORKED FOR THIS CANDIDATE" prompt section, so every AI feature knows which resumes got replies rather than just the overall response rate. Below the threshold the prompt explicitly tells the model not to claim it knows what works yet. Env vars are now read per call instead of at module scope
  • •app/api/outcomes/route.ts (new): read-only endpoint exposing the analysis to the tracker
  • •components/OutcomeInsights.tsx (new): "What is working" panel on the tracker. Below the threshold it shows progress toward it, so the value is legible before there is enough data to prove it; above it, the factors that separate replies from silence
  • •app/api/applications/route.ts: POST now accepts resume_doc_id and cover_letter_doc_id, verified under RLS to belong to the caller before insert (the FKs only check that a row exists, not who owns it). The same ownership check was added for ats_score_id, which was previously stored unverified
  • •app/api/applications/[id]/route.ts: PATCH accepted resume_doc_id, cover_letter_doc_id, and ats_score_id with no ownership check at all, so a caller could attach another user's document or score to their own application and have it read back. All three are now verified against the caller, matching the check job_id already had
  • •app/(dashboard)/tracker/page.tsx: manually logged applications can attach the resume used, which previously was always null and left manual entries outside the analysis entirely
  • •app/(dashboard)/tracker/page.tsx: fixed "Avg ATS score" showing NaN after logging an application. The POST response has no overall_score key (the score is joined in only by the GET handler), and the average filtered on !== null, which let undefined through into the sum. Now filters on typeof number, and the saved row normalises overall_score to null
  • •tests/lib/outcomes.test.ts, tests/lib/userContextPrompt.test.ts (new): 12 tests covering bucket thresholds, pending-application exclusion, unscored-row handling, and the prompt wording at each confidence level
  • •no new DB columns, no new env vars, no packages
Jul 18, 2026
Fix

Sentry no longer records errors thrown by in-app browser scripts

  • •instrumentation-client.ts: added ignoreErrors for "window.webkit.messageHandlers", "_AutofillCallbackHandler", and "instantSearchSDKJSBridgeClearHighlight" - errors thrown by scripts that the Instagram/Facebook iOS in-app browsers inject into every page they open, not by Vantage code (seen in Sentry from a link-in-bio visitor)
  • •no new DB columns, no new env vars, no packages
Jul 18, 2026
Update

Vantage now has a Discord community

  • •components/marketing/MarketingFooter.tsx: Discord icon added to the footer social row and a "Discord" text link added to the Resources column (both open https://discord.gg/GqHryVbKyc in a new tab); footer link columns now support external links
  • •no new DB columns, no new env vars, no packages
Jul 18, 2026
Improvement

Feedback is easier to reach at the moment it matters

  • •components/ui/ErrorNotice.tsx (new): shared inline error box with a "Report this bug" link to /feedback?type=bug, so failures can be reported the moment they happen
  • •components/FeatureFeedbackNudge.tsx (new): small dismissible "How did that go?" line shown after successful actions, heavily rate limited - only after 3 lifetime successes, at most once every 14 days, and it shares the dismissed-at localStorage key with the dashboard feedback card so the two prompts never stack; dismissing either silences both
  • •app/(dashboard)/tailor/page.tsx: all five inline error boxes replaced with ErrorNotice; job parse, ATS score, resume tailoring, and cover letter successes now count toward the nudge, which renders at the bottom of step 3
  • •app/(marketing)/feedback/FeedbackForm.tsx: subject dropdown preselects from a ?type= query param (reads window.location so the page stays static)
  • •no new DB columns, no new env vars, no packages
Jul 18, 2026
Improvement

Every error box across the app now has a "Report this bug" link

  • •components/ui/ErrorNotice.tsx: gained an optional onDismiss prop for error boxes that had their own dismiss button
  • •app/(dashboard)/networking/page.tsx, interview/page.tsx, ats/page.tsx, tracker/page.tsx, jobs/page.tsx, apply/[jobId]/page.tsx: inline error boxes replaced with the shared ErrorNotice (report link included); AI successes on networking, interview, ATS, apply, and strategy now count toward the rate-limited feedback nudge
  • •app/(dashboard)/strategy/StrategyReport.tsx: the error banner keeps its Retry button and adds a report link; a forced report refresh counts as a feature success
  • •components/ResumeUpload.tsx, ProfileForm.tsx, ExtensionTokenSection.tsx, SettingsClient.tsx, ResumeStudio.tsx: error displays converted to ErrorNotice
  • •intentionally untouched: auth pages (wrong-credential errors are not bugs), admin components, and tiny inline span errors inside flex rows (networking log row, jobs preset row, application modal, help chat) where a block box would break the layout
  • •no new DB columns, no new env vars, no packages
Jul 17, 2026
Update

Chrome extension is live on the Chrome Web Store

  • •the Vantage Auto-Fill extension is published: https://chromewebstore.google.com/detail/vantage-auto-fill/mgapanbbaplohlojbmghoglmpfpogook
  • •app/(dashboard)/apply/page.tsx: the "Get the extension" setup button now opens the store listing in a new tab (CHROME_EXTENSION_URL placeholder replaced); removed the "Not on the Chrome Web Store yet" note
  • •app/(dashboard)/apply/[jobId]/page.tsx: the "Install extension" button in the Auto-fill card now points at the real listing instead of the generic Web Store homepage
  • •app/(marketing)/docs/extension/page.tsx: install section now links the listing directly; removed the developer-mode install section; bumped Last updated to July 17, 2026
  • •app/(marketing)/docs/auto-fill/page.tsx: setup step 1 links the store listing instead of saying developer mode is required
  • •components/ExtensionTokenSection.tsx: Profile's Browser Extension card links the store listing for anyone who has not installed yet
  • •lib/appKnowledge.ts: help chat now knows the extension is named "Vantage Auto-Fill" and installs from the Chrome Web Store, with the listing URL
  • •content/blog/vantage-is-officially-live.mdx: the Chrome extension bullet now links to the listing
  • •no new DB columns, no new env vars, no packages
Jul 17, 2026
Fix

Extension connect always failed with "Invalid token"; hydration errors on date labels

  • •extension/popup.js: the popup called https://vantage-ai.dev, but the apex domain 308-redirects to www.vantage-ai.dev and browsers strip the Authorization header on cross-origin redirects - so every token verification and fill request arrived unauthenticated and got a 401, making valid connection codes look invalid. VANTAGE_URL now points at https://www.vantage-ai.dev directly; manifest version bumped to 1.1.1 (needs a re-upload to the Chrome Web Store)
  • •components/ExtensionTokenSection.tsx: the "Created <date>" label formatted the timestamp in the viewer's local timezone, which can differ from the server-rendered UTC date and trigger React hydration error 418 on the Profile page (seen in Sentry). Now formats with timeZone UTC so server and client render identical text
  • •components/DocumentsClient.tsx: document dates used the browser's default locale and timezone against server-rendered HTML - same hydration mismatch on the Documents page. Now en-US + UTC
  • •components/ApplicationDetailModal.tsx: applied_date is a DATE column, so local-timezone formatting showed the day before in negative-offset timezones; now formatted in UTC (display fix, modal is client-only so no hydration risk)
  • •no new DB columns, no new env vars, no packages
Jul 17, 2026
Update

Auto-fill is extension-only - console snippet and answers panel removed

  • •deleted app/api/generate-fill-snippet/route.ts and app/api/analyze-form/route.ts: the old Tier 2 (paste-into-console snippet) and Tier 3 (copy-paste answers panel) auto-fill fallbacks had no UI entry points left and are gone for good; the Chrome extension is the one auto-fill path
  • •lib/rateLimit.ts: removed the analyze-form rate-limit spec, so the Usage Limits page no longer lists "Application form analyses"
  • •lib/appKnowledge.ts: help chat now describes auto-apply as extension-only instead of "three ways"
  • •app/(marketing)/docs/extension/page.tsx: notes the extension is the only fill method; new troubleshooting entry for a valid connection code being rejected by an outdated extension version (update via chrome://extensions)
  • •CLAUDE.md, README.md, docs/legal-dossier.md: auto-apply sections rewritten around the extension; tier language removed
  • •no new DB columns, no new env vars, no packages
Jul 16, 2026
Fix

Race-proof monthly quota counting

  • •supabase/migrations/20260716_atomic_subscription_increment.sql: new increment_subscription_use() SQL function does the usage-counter increment atomically in one statement, with a column whitelist and service-role-only access; also locks down increment_platform_limit() so clients can no longer call it directly
  • •lib/rateLimit.ts: consumeLimit() now calls the atomic SQL function first and only falls back to the old compare-and-swap loop while the function is missing from the database, so deploying before running the migration is safe
  • •no new env vars, no packages
Jul 16, 2026
Improvement

Error monitoring with Sentry

  • •Sentry wizard setup: sentry.server.config.ts, sentry.edge.config.ts, instrumentation.ts, instrumentation-client.ts, app/global-error.tsx, next.config.ts wrapped with withSentryConfig
  • •lib/apiResponse.ts: serverError() now reports the caught exception to Sentry - routes catch all their own errors, so without this Sentry would never see API failures
  • •lib/ai.ts: reports to Sentry when the platform AI budget passes 80% (warning) and when it is exhausted (error), so AI features going down is never silent
  • •middleware.ts: matcher no longer intercepts /monitoring, the Sentry browser tunnel route
  • •Sentry trace sampling reduced to 10% in production to protect the free-plan quota
  • •removed the wizard's public example pages (app/sentry-example-page, app/api/sentry-example-api)
  • •new package: @sentry/nextjs; auth token lives in .env.sentry-build-plugin (gitignored)
Jul 16, 2026
Improvement

Reorganized test suite with full unit coverage

  • •all tests now live under tests/ (tests/lib, tests/components, tests/static) mirroring the source tree; the 21 old test files scattered through app/, components/, and lib/ are deleted
  • •new coverage: lib/validateRequest, lib/withTimeout, lib/apiResponse (incl. the Sentry capture), lib/rateLimit (dev-mode short-circuits, admin bypass, pricing-contract limits), lib/rateLimitMessage, lib/ai (mocked Gemini: budget gate, 80% warning, JSON fence parsing), lib/resumeHtml (sanitizer XSS stripping, linkify, bullet tailoring)
  • •lib/rateLimitMessage.ts: fixed a real bug the new tests caught - every rate-limit wait was reported in hours ("try again in 1 hour" for a 2-minute wait) because the minutes branch was unreachable
  • •repaired the three previously-failing tests: invalid-date generator in the auto-apply property test, and stale design allowlists that predated the gold/glass landing redesign
  • •vitest.config.ts: include pattern narrowed to tests/**; .gitignore: *.test.ts / *.test.tsx removed so the suite is version-controlled
  • •247 tests, all passing; no new env vars, no packages
Jul 16, 2026
Improvement

Codebase-wide comment audit

  • •rewrote comments across app/, lib/, components/, and extension/ so they read as first-person developer notes: removed spec/requirement references (Requirement N.N, Property N, rule N), step-by-step plan narration, and redundant what-the-next-line-does comments
  • •replaced them with comments that explain the non-obvious: why ATS scoring shares the tailoring quota, the apply-page id resolution order, particle recycling in the landing canvas, extension fill guarantees
  • •comment-only change - verified with tsc, node --check on extension JS, and the full 247-test suite
  • •no DB changes, no new env vars, no packages
Jul 15, 2026
Fix

Failed requests no longer count toward usage limits

  • •lib/rateLimit.ts: checkLimit() and checkRateLimit() are now read-only checks; new consumeLimit() and recordRateLimitUse() charge the monthly quota and per-route rate limit only when a route reaches its success path - AI errors, timeouts, upstream failures, and database errors no longer burn a use
  • •every rate-limited route updated to charge on success only: tailor-resume, ats-score, generate-cover-letter, parse-job, answer-question, analyze-form, extension/ai-fill, generate-fill-snippet, interview-prep generate + assess, generate-outreach, find-contacts, discover-jobs, strategy-feedback, parse-resume, parse-profile, app-chat, resume-studio + pdf, tailor-resume/render, and the example template route
  • •parse-job, analyze-form, extension/ai-fill, resume-studio, and resume-studio/pdf now validate the request body before the limit checks, so a bad request costs nothing (previously an invalid Resume Studio body still burned an edit)
  • •app/api/find-contacts/route.ts: a SerpApi failure now returns an error instead of a misleading "no contacts found" success (and is not charged)
  • •app/api/extension/ai-fill/route.ts: the degraded partial result returned when the AI fails (deterministic answers only) is no longer charged
  • •lib/appKnowledge.ts: Usage Limits section notes that only successful runs count
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Fix

Junk job parses (title "." and nothing else) are rejected instead of saved

  • •app/api/parse-job/route.ts: after the AI parse, the job must have a real title plus at least one of required skills, responsibilities, or keywords - empty-shell parses (usually login walls or scraper-block pages behind the URL) now return a clear 422 pointing at pasting the description instead, and are not charged against any limit
  • •lib/jobFilters.ts: hasRealJobTitle() now also rejects punctuation-only strings (".", "---") and common AI placeholder values ("Unknown", "N/A", "Not specified") - this also cleans those out of strategy-feedback role grouping and the auto-apply job listing
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Improvement

Gemini spend hardening - platform-wide AI budget and pro strategy-feedback cap

  • •lib/ai.ts: callGemini() now enforces a platform-wide budget of 3000 Gemini calls per 30 days (platform_limits key gemini_monthly) - an absolute spend ceiling (~$10/month worst case) that holds no matter how many accounts sign up; when exhausted, AI features return the friendly "over capacity" 429 until the window resets, and the counter only increments on calls Google actually served
  • •app/api/strategy-feedback/route.ts: pro users previously skipped the per-route rate limit entirely, and ?force=true bypasses the result cache - an unbounded AI path; pro now has a hidden 10/day abuse cap (dev 2/day and free 2/month unchanged; the /limits page still shows pro as unlimited)
  • •full audit confirmed the Gemini SDK is imported only in lib/ai.ts and all 18 routes that call it check a rate limit before the AI call
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Update

Tighter pro daily caps to keep worst-case AI spend under the subscription price

  • •pro per-day limits lowered: Resume Studio edits 60 to 30, job parsing 12 to 10, application question answers 20 to 10, interview answer feedback 15 to 10, strategy feedback hidden abuse cap 10 to 5
  • •app/api/resume-studio, parse-job, answer-question, interview-prep/assess, strategy-feedback route configs and RATE_LIMIT_SPECS in lib/rateLimit.ts updated together
  • •worst-case fully-maxed pro user drops from roughly $12/month to roughly $8/month in Gemini costs
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Update

Example endpoint no longer calls the AI; Resume Studio pro cap 15/day; platform AI budget raised to ~$15

  • •app/api/example/route.ts: the live reference template now echoes the prompt instead of calling Gemini - nothing in the app calls it, so it should not be able to spend tokens; a comment marks where the AI call goes when copying the pattern
  • •app/api/resume-studio/route.ts + lib/rateLimit.ts RATE_LIMIT_SPECS: pro Resume Studio edits lowered from 30/day to 15/day
  • •lib/ai.ts: PLATFORM_MONTHLY_CALL_BUDGET raised from 3000 to 4500 calls per 30 days (~$15/month absolute worst-case Gemini spend)
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Update

Privacy Policy expanded with data-collection and rights detail

  • •app/(marketing)/privacy/page.tsx: added a "What we don't collect" section (no sensitive personal information, no data about you sourced from third parties), a "Business transfers" section, and a Do-Not-Track note under Cookies
  • •Usage data entry now explicitly names IP address and browser/device type among the technical logs collected
  • •Google (sign-in) entry now notes compliance with Google's API Services User Data Policy, including Limited Use
  • •Your rights section gained an opt-out-of-sale/targeted-advertising bullet and a note that non-Canadian users' local rights will be honored on request
  • •docs/legal-dossier.md updated to mirror the same facts
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Update

Pro pricing displayed as CAD instead of USD

  • •app/(marketing)/page.tsx: pricing card now reads "$8 CAD/month"; structured-data offer priceCurrency changed from USD to CAD
  • •app/(marketing)/terms/page.tsx, app/(marketing)/docs/billing/page.tsx, app/(dashboard)/billing/page.tsx, lib/appKnowledge.ts: "$8/month" copy now says CAD instead of (or in addition to) USD
  • •app/admin/page.tsx: formatMoney() now formats MRR as CAD (en-CA/CAD) instead of USD - display-only, does not change what Stripe actually charges
  • •CLAUDE.md and docs/legal-dossier.md updated to match
  • •owner action still required: the Stripe Price behind STRIPE_PRO_PRICE_ID must itself be a CAD price for checkout to actually charge CAD - currency lives on the Stripe Price object, not in this codebase, and Stripe prices can't be edited in place, so a new CAD price must be created and the env var repointed to its id (in .env.local and Vercel)
  • •no new DB columns, no new npm packages
Jul 15, 2026
Improvement

Loading status while your resume auto-fills your profile

  • •components/ProfileForm.tsx: added a parsing state that shows a status banner ("Reading your resume and filling in your profile... please stay on this page") after a resume upload finishes, covering the gap while /api/parse-profile runs and the fields populate
  • •no change to upload/parse functionality; purely a visible loading indicator during the existing buffer
  • •no new DB columns, no new env vars, no new npm packages
Jul 15, 2026
Update

Removed the self-serve refund feature

  • •app/api/stripe/refund/route.ts deleted: the self-serve refund endpoint is gone
  • •components/BillingActions.tsx: removed the RefundButton component
  • •app/(dashboard)/billing/page.tsx: removed the money-back guarantee note and refund button from the Pro plan card
  • •app/(marketing)/page.tsx, app/(marketing)/terms/page.tsx: pricing/terms copy no longer mentions a money-back guarantee; refund policy is now non-refundable, cancel-anytime with access to period end
  • •docs/legal-dossier.md updated to match
  • •no DB changes, no new env vars, no packages
Jul 15, 2026
Update

Removed the 7-day free trial

  • •app/api/stripe/checkout/route.ts: dropped trial_period_days, so Pro billing starts immediately on upgrade
  • •app/api/stripe/webhook/route.ts: no longer maps 'trialing' status to pro or writes trial_end; plan is pro only when the subscription is active
  • •lib/rateLimit.ts: resolvePlan treats only 'active' pro subscriptions as pro (trialing removed)
  • •app/(dashboard)/billing/page.tsx: removed the trial badge and trial copy from the plan card
  • •app/(marketing)/page.tsx: Pro CTA is now "Upgrade to Pro" with "$8 CAD/month, cancel any time"
  • •app/(marketing)/terms/page.tsx, docs/legal-dossier.md: removed free-trial language
  • •subscriptions.trial_end column is left in place but no longer written or read
  • •no new env vars, no packages
Jul 15, 2026
Fix

Clearer login error when the email is not verified

  • •app/(auth)/login/page.tsx: sign-in failures caused by an unverified email now show "Please verify your email first..." instead of the generic "We could not sign you in" message (detects Supabase's email_not_confirmed code)
Jul 14, 2026
Update

Resume Studio in the sidebar, tighter dev limit, louder feedback prompt

  • •components/Sidebar.tsx: Resume Studio now appears in the sidebar with a gold "Experimental" tag; the isNew flag was generalized to a badge string so Auto-apply's "New" and this share one style
  • •app/api/resume-studio/route.ts + lib/rateLimit.ts: dev-tier limit lowered from 30 to 10 edits/day (free 20/month and pro 60/day unchanged)
  • •components/FeedbackNudge.tsx + app/dashboard/page.tsx: the feedback prompt moved from the bottom of the dashboard to directly under the page header, with a bolder "Found a bug? Missing a feature?" headline and a filled Share feedback button (still one-time dismissible)
  • •app/(dashboard)/resume-studio/page.tsx: stale "unlisted" comment updated
  • •no new DB columns, no new env vars, no new npm packages
Jul 14, 2026
Fix

Feedback prompt resurfaces weekly; sidebar footer pinned to the bottom

  • •components/FeedbackNudge.tsx: dismissing now stores a timestamp instead of a permanent flag - the prompt comes back after 7 days (users who dismissed under the old permanent flag see it again once, then the weekly cycle applies)
  • •components/Sidebar.tsx: on short viewports the nav list overflowed and pushed the footer's link rows past the screen edge, leaving a stranded divider line; the nav now scrolls within the rail (flex minHeight 0 + overflow-y auto), the footer never shrinks, and trailing padding was trimmed so the link rows sit flush at the bottom
  • •no new DB columns, no new env vars, no new npm packages
Jul 14, 2026
Feature

Tailor + ATS full-resume mode, tailoring-focused Resume Studio, hyperlink-preserving PDFs, rate-limit hardening

  • •lib/extractResumeText.ts: new extractResumeLinks() pulls hyperlink destinations out of uploaded resumes (PDF link annotations via pdf.js, DOCX via mammoth HTML) - plain-text extraction was silently dropping every link
  • •lib/resumeHtml.ts created: shared resume-HTML helpers - prompt rules, sanitizer, linkifyResumeHtml() (deterministically re-attaches known links and anchors bare URLs/emails), and applyChangesToHtml() (swaps tailored bullets into profiles.resume_html without AI)
  • •app/api/resume-studio/route.ts: upload extraction now returns the file's links; the generate prompt re-attaches them and every generate/edit response passes through the linkify safety net, so downloaded PDFs keep the hyperlinks the original resume had
  • •app/api/tailor-resume/render/route.ts created: renders a tailored_resume document as full resume HTML - surgical bullet-swap into the stored base-resume HTML when possible (fast, free, link-preserving), one rate-limited AI formatting call as fallback
  • •app/(dashboard)/tailor/page.tsx: the Tailored Resume tab now has two views - "Bullet changes" (the existing diff/copy flow) and "Full resume" (rendered preview + Download PDF named after the company/role)
  • •components/ResumeStudio.tsx + app/(dashboard)/resume-studio/page.tsx: the studio is now tailoring-focused - start from a resume tailored on Tailor + ATS (with skill-gap/keyword suggestion chips that prefill edit instructions) or your base resume, alongside the existing file upload; tailoring itself stays on Tailor + ATS
  • •Rate-limit audit across every AI/external API route (all 17 Gemini routes verified limited, limiter always before the AI call):
    • •app/api/extension/ai-fill/route.ts, app/api/analyze-form/route.ts, app/api/generate-fill-snippet/route.ts: all three auto-apply tiers now charge the monthly auto_apply feature quota (previously only per-route sliding windows, and tier 2 had no limits at all)
    • •app/api/discover-jobs/route.ts: platform-wide Adzuna budget (900 calls/30 days via platform_limits) so multi-user refresh traffic cannot blow the 1000/month free tier
    • •app/api/resume-studio/pdf/route.ts: per-user rate limit on PDF rendering (headless Chromium is expensive even without AI)
    • •lib/rateLimit.ts: RATE_LIMIT_SPECS gains render-resume (5/day dev, 10/mo free, 10/day pro) and resume-pdf (20/40/60) so the /limits page stays accurate
  • •no new DB columns, no new env vars, no new npm packages
Jul 14, 2026
Fix

Tailor + ATS results tabs no longer send you backwards to run the next action

  • •app/(dashboard)/tailor/page.tsx: the ATS Score tab's empty state now runs "Check ATS Score" in place (the tailored resume and cover letter stay put) instead of "Go back to check score", which stranded users on step 2 with no way to return to their results; the Tailored Resume tab's empty state got the same in-place "Tailor My Resume" treatment, and both show action errors inline
  • •app/(dashboard)/tailor/page.tsx: step 2 now shows a "You already have results for this job - View results" banner whenever a tailored resume, ATS score, or cover letter exists, so stepping back is never a one-way door
  • •no new DB columns, no new env vars, no new npm packages
Jul 14, 2026
Update

Help chat knowledge base caught up with today's features

  • •lib/appKnowledge.ts: the chat widget now knows about the Tailor + ATS "Bullet changes"/"Full resume" views with PDF download, in-place tab actions, Resume Studio's tailoring-focused start sources and suggestion chips, hyperlink preservation in generated PDFs, auto-apply credits applying to all three fill tiers, and the per-feature rate limits shown on Usage Limits
  • •no new DB columns, no new env vars, no new npm packages
Jul 13, 2026
Feature

Rewrite generated outreach messages with a custom instruction

  • •app/(dashboard)/networking/page.tsx: after a message is generated, a new "Want changes? Describe them" input lets you type an instruction (e.g. "make it shorter", "mention my co-op") and rewrite the message in place; Enter or the Rewrite button submits, capped at 300 characters
  • •app/api/generate-outreach/route.ts: refine mode (refineMessageId + refineInstruction) was already in place - rewrites the saved message per the instruction and counts toward the same networking limits and rate limits as generating
  • •no new DB columns, no new env vars, no new npm packages
Jul 13, 2026
Major Release

Google sign-in, help chat, product emails, data reset, Resume Studio, and a full audit pass

  • •app/auth/callback/route.ts created + login/signup pages: "Continue with Google" via Supabase OAuth; first-time Google users get their name backfilled and land on profile onboarding (requires Google provider setup in Supabase + Google Cloud)
  • •components/HelpChatWidget.tsx + app/api/app-chat/route.ts + lib/appKnowledge.ts: floating help chat on every dashboard page that answers questions about how Vantage works from a built-in knowledge base; it refuses to generate documents and points to the right feature page instead (rate limited: dev 5/day, free 50/month, pro 10/day)
  • •lib/ai.ts: new generateChat() for multi-turn conversations; retry logic factored into one shared Gemini call path
  • •lib/email.ts + app/api/admin/send-campaign/route.ts + components/AdminCampaignForm.tsx: product-update emails via Resend, composed and sent from /admin with a mandatory "send test to me" flow; every email carries one-click unsubscribe headers and a footer link
  • •app/api/unsubscribe/route.ts + app/(marketing)/unsubscribed/page.tsx: tokenized one-click unsubscribe, no login needed; components/SettingsClient.tsx gains a "Product updates" toggle
  • •app/api/account/reset/route.ts + Settings danger zone: "Reset my data" wipes all app data and uploaded files but keeps the account and subscription (type RESET to confirm); sql/one_time_wipe_2026-07.sql added for the owner-run global fresh start
  • •app/api/admin/reset-analytics/route.ts + components/AdminResetAnalytics.tsx: admin Maintenance section clears events and route logs (type RESET ANALYTICS)
  • •components/FeedbackNudge.tsx on the dashboard home + a "Send feedback" link in the help chat header: the /feedback page is now discoverable in-app (one-time dismissible)
  • •app/(dashboard)/resume-studio/ (unlisted URL /resume-studio) + app/api/resume-studio/: experimental "edit your resume by chatting" - upload a PDF/Word file, give instructions like "cut the summary to two lines", watch the live preview update, download the PDF; nothing is stored server-side (lib/extractResumeText.ts + lib/resumeCss.ts refactored out for reuse)
  • •Security pass: print-PDF iframe now strips executable content; extension token and resume-parse debug logging removed; Stripe webhook/checkout/parse-job logs no longer dump payloads; Resume Studio preview renders in a scriptless sandboxed iframe; audit confirmed webhook signature checks, storage policies, admin guards, and per-user scoping across all routes
  • •Scalability pass: buildUserContext now fetches its six sources in parallel; rate-limit service client is reused across calls; subscriptions query selects explicit columns; admin dashboard caps events/route_logs reads at the newest 50k rows; new indexes on rate_limit_logs, events, route_logs, applications
  • •Comment pass across lib/, components/, and every API route: file headers + JSDoc on exports; CLAUDE.md corrected (AI provider is Gemini via lib/ai.ts, not Groq)
  • •docs/legal-dossier.md created: full data inventory, processor list, and the owner questions needed to generate ToS/privacy/compliance docs with a legal AI tool
  • •new DB columns: profiles.marketing_emails_enabled, profiles.unsubscribe_token + 4 indexes (supabase/migrations/20260713_batch3.sql - run in SQL editor)
  • •new env vars: RESEND_API_KEY, RESEND_FROM_EMAIL
  • •packages installed: resend
Jul 13, 2026
Update

Legal pages rewritten to match the current product, plus a Data Compliance page

  • •app/(marketing)/terms/page.tsx: updated for Google sign-in consent, the 7-day self-serve refund window (the old "no refunds" line contradicted the billing page), trials, opt-in email rules, an experimental-features clause covering Resume Studio, and a changes-to-terms section
  • •app/(marketing)/privacy/page.tsx: removed the stale Cerebras section (all AI runs on Gemini), added Google sign-in, Resend, Adzuna, SerpApi, and the feedback webhook to the third-party list, and documented networking contact data, voice input for interview practice, help chat handling, cookies/browser storage, Word resume uploads, and the new Reset my data control
  • •app/(marketing)/data-compliance/page.tsx created: plain-language PIPEDA posture page with safeguards, the full subprocessor list, AI processing commitments, user controls, and breach notification; linked from the marketing footer Legal column and cross-linked from terms + privacy
  • •docs/legal-dossier.md: rewritten as a single self-contained prompt to paste into a legal chatbot (instructions + facts + [FILL IN] markers), prefilled with the facts already stated on the site (Ontario law, age 16+, PIPEDA, contact email)
  • •no new DB columns, no new env vars, no new npm packages
Jul 12, 2026
Fix

Auto-apply question answering, account-switch stale data, ATS average skew, trial-status webhook

  • •app/(dashboard)/apply/[jobId]/page.tsx + app/api/answer-question/route.ts: question generation no longer sends the route param (an application id) as a job id; the API resolves the application first and treats the jobs lookup as optional, so manual applications (no jobs row) can generate answers instead of erroring "Job not found"
  • •app/(auth)/login/page.tsx + components/Sidebar.tsx: router.refresh() on sign-in and sign-out so the Router Cache never serves the previous account's name/email/data; sign-out also clears per-user localStorage caches
  • •app/(dashboard)/jobs/page.tsx: job feed localStorage cache is now keyed by user id (jobFeedCache:<userId>); the legacy shared key is dropped on load
  • •lib/userContext.ts: average ATS scores now ignore rows with null scores instead of counting them as 0 (this was dragging the average shown in strategy feedback)
  • •app/api/stripe/webhook/route.ts: customer.subscription.updated treats status 'trialing' as plan 'pro'; both handlers store trial_end
Jul 12, 2026
Fix

Job pickers source from the application tracker (networking, ATS)

  • •app/(dashboard)/networking/page.tsx + app/(dashboard)/ats/page.tsx: job dropdowns now list tracked applications (applications table first, enriched from jobs), matching the interview page pattern - manual tracker entries appear instead of only tailored jobs
  • •ATS page: manual applications without job details show "(needs job details)" and prompt to add the posting from the tracker before scoring
  • •networking generate-outreach accepts jobTitle/jobCompany as plain-text context for manual applications (app/api/generate-outreach/route.ts)
Jul 12, 2026
Improvement

Substantive resume tailoring, personalized strategy feedback, identity-first outreach, strict contact role filter

  • •app/api/tailor-resume/route.ts: rewritten prompt bans pure-conciseness and verb-swap-only edits, requires each change to add job terminology / mirror a responsibility / re-frame toward the job's domain, and a server-side filter drops trivial edits the model still returns; per-bullet changes are persisted to documents.changes (new jsonb column)
  • •app/api/strategy-feedback/route.ts: output now addresses the user as "you" instead of "the candidate"
  • •app/api/generate-outreach/route.ts: messages lead with the sender's identity (university/program), cite concrete current work and a flagship project with real technologies, and end with a direct ask; company-flattery openers are banned
  • •app/api/find-contacts/route.ts: an optional target role is now a hard filter (query built around the role + title/snippet post-filter); zero matches return a suggestion to remove the role or try a different one, surfaced on the networking page
  • •new DB column: documents.changes jsonb (supabase/migrations/20260712_batch_fixes.sql)
Jul 12, 2026
Feature

Full tailored resume hidden everywhere - users see only the per-bullet diff

  • •components/TailorDiff.tsx created: shared per-bullet diff view + formatTailoredBullets() (grouped tailored-bullets copy text)
  • •app/(dashboard)/tailor/page.tsx: "Copy all tailored bullets" now copies only the tailored bullets; the full-resume fallback <pre> is gone
  • •app/(dashboard)/apply/[jobId]/page.tsx: kit card shows the change count + copy-bullets button instead of a raw resume preview
  • •components/ApplicationDetailModal.tsx + app/api/applications/[id]/details/route.ts: the API strips tailored-resume content server-side and returns changes; the modal renders the diff (cover letters unchanged, PDF removed for tailored resumes)
  • •app/(dashboard)/documents/page.tsx + components/DocumentsClient.tsx: tailored resumes render the diff and copy bullets only; content is nulled before reaching the client
  • •documents.content is still saved for internal use (ATS scoring, extension auto-fill) but never rendered
Jul 12, 2026
Feature

Manual outreach logging, full application editing, tailor from the tracker

  • •app/api/outreach-messages/route.ts created: POST logs a user-written message (is_manual=true, new DB column) - no AI, no usage limits
  • •app/(dashboard)/networking/page.tsx: "Log a message" panel in the outreach tracker for messages sent outside Vantage
  • •app/api/applications/[id]/route.ts: PATCH now accepts company, role, job_url, and job_id (job ownership verified)
  • •components/ApplicationDetailModal.tsx: edit mode for company/role/URL/date/status/notes, plus a Tailor resume action - job-linked applications jump to /tailor?jobId=..., manual ones paste the posting once (parse-job), get linked, then tailor; tracker rows sync via a new onUpdated callback
  • •app/(dashboard)/tracker/page.tsx: passes onUpdated to keep the table in sync with modal edits
Jul 12, 2026
Feature

Job finder - saved named filters, km-radius search, salary/date filters wired to Adzuna

  • •app/api/discover-jobs/route.ts: refresh now forwards where + distance (km), salary_min, max_days_old, and employment-type flags to Adzuna (salaryMin/datePosted were previously collected but unused)
  • •app/(dashboard)/jobs/page.tsx: radius row (city/postal input, 10/25/50/100 km select, "Use my location" via browser geolocation reverse-geocoded to a city) and a presets row (save current filters under a name, apply, delete)
  • •app/api/job-filter-presets/route.ts + [id]/route.ts created: GET/POST/DELETE, capped at 20 presets per user
  • •new DB table: job_filter_presets (RLS per-user) in supabase/migrations/20260712_batch_fixes.sql
Jul 12, 2026
Feature

7-day free trial and 7-day self-serve refund on Pro

  • •app/api/stripe/checkout/route.ts: subscription checkout starts with trial_period_days 7 (card upfront, billing after the trial, cancellable any time)
  • •app/api/stripe/refund/route.ts created: refunds the latest paid invoice if it is within 7 days, cancels the subscription, downgrades to free; clear error with the cutoff date otherwise
  • •app/(dashboard)/billing/page.tsx + components/BillingActions.tsx: trial badge with "Trial ends <date>", money-back note, and a confirm-step Request refund button
  • •app/(marketing)/page.tsx pricing: Pro card CTA is "Start 7-day free trial" with trial + money-back copy
  • •new DB column: subscriptions.trial_end (supabase/migrations/20260712_batch_fixes.sql)
Jul 12, 2026
Improvement

Mobile and tablet responsiveness pass across the app

  • •app/globals.css: new .rsp-grid-2/.rsp-grid-3 helpers (fixed multi-column grids collapse to one column under 640px) and .tracker-table-row (drops the hidden "Since" track on phones instead of reserving empty space)
  • •converted fixed-column grids to the helpers: ProfileForm (profile fields, experience, projects), networking (search/generate forms + skeletons), interview (manual job form, strengths/improvements), apply page kit + skeleton
  • •app/(dashboard)/jobs/page.tsx: filter bar controls use flexible widths (flex + max-width) instead of hardcoded pixel widths
  • •no visual redesign - layout only responds below existing breakpoints; tables keep their overflow-x scroll wrappers
Jul 12, 2026
Fix

Second-round fixes - tracker-only listings, mobile sidebar, AI capacity errors, job feed persistence

  • •app/(dashboard)/ats/page.tsx: score history only shows jobs that are tracked applications (no more "Untitled job" rows from tailoring runs that were never logged)
  • •app/(dashboard)/documents/page.tsx: the documents list is likewise filtered to tracked applications
  • •components/Sidebar.tsx: the desktop sidebar is now actually hidden under 768px (it previously stayed fixed over the page, breaking every mobile view); the drawer + topbar take over as designed
  • •app/(dashboard)/networking/page.tsx: manual-log inputs use the page background + border so they are visually separated from the raised panel
  • •app/(dashboard)/jobs/page.tsx: filter bar redesigned into labelled controls with the saved-filters group on the right; distance/radius search removed entirely per feedback (app/api/discover-jobs no longer forwards where/distance)
  • •app/api/discover-jobs/route.ts: job relevance scoring is ONE batched Gemini call instead of one call per job (a single refresh used to burn the free tier's whole daily quota and break every other AI feature); scoring failures degrade to a neutral score instead of failing the feed
  • •app/api/discover-jobs/route.ts: each job's link is resolved server-side through Adzuna's redirect (following the details page's apply link when needed) so "View job" opens the employer's actual posting
  • •app/(dashboard)/jobs/page.tsx: on load the feed restores from the database when the per-user cache is empty, so signing out and back in no longer empties the page
  • •app/(dashboard)/apply/[jobId]/page.tsx: back links go to /apply (not the tracker); the saved per-bullet diff is viewable inline via "View changes" without re-running tailoring
  • •lib/ai.ts: Gemini 503 "high demand" responses are retried like 429s; new isAiQuotaError()/AI_BUSY_MESSAGE map capacity failures to a friendly 429 in tailor-resume, ats-score, generate-cover-letter, generate-outreach, answer-question, interview-prep generate/assess, and strategy-feedback (previously these surfaced as generic 500s)
  • •app/api/generate-outreach/route.ts: identity line in the prompt is a placeholder pattern so blank profiles can never echo the example school
  • •no new DB columns, no new env vars, no new npm packages
Jul 12, 2026
Fix

Third-round fixes - scored_at bug, desktop-only gate, working salary/date filters, usage limits page

  • •app/(dashboard)/apply/[jobId]/page.tsx + app/api/applications/[id]/details/route.ts: ATS score lookups ordered by created_at, a column ats_scores does not have (it has scored_at) - the whole query errored silently, which is why a freshly generated ATS score never appeared in auto-apply or the tracker modal
  • •app/dashboard/page.tsx: the Avg ATS Score stat now only counts scores belonging to tracked applications (same rule as /ats)
  • •app/(dashboard)/jobs/page.tsx + app/api/discover-jobs/route.ts: Min salary and Posted filters now actually filter - Adzuna's salary bounds and posted date are stored in raw_data on refresh and applied client-side (jobs fetched before the change are kept until a refresh backfills them)
  • •app/api/discover-jobs/route.ts: removed the job-URL resolver - Adzuna serves 403 to server-side requests and its API deliberately omits the employer URL, so redirect_url (which forwards real browsers to the source) is the only supported link
  • •components/Sidebar.tsx: reverted the sidebar to inline flex layout, fixing the footer floating mid-rail
  • •app/(dashboard)/layout.tsx + app/dashboard/layout.tsx + globals.css: the dashboard is now desktop-only - under 768px the shell is hidden and a "Vantage is built for desktop" notice renders instead
  • •app/api/generate-outreach/route.ts: prompt rebuilt around a style exemplar (flowing prose, purpose-driven work descriptions, one bold ask); connection requests compress to a single work clause to stay under LinkedIn's 280 characters
  • •app/(dashboard)/networking/page.tsx: autoComplete="off" on all inputs so browsers stop offering to save contact fields as an address/ID card
  • •lib/rateLimit.ts: resolvePlan treats status 'trialing' as pro (consistent with the webhook); new RATE_LIMIT_SPECS + getRateLimitOverview() compute per-feature usage from rate_limit_logs
  • •app/(dashboard)/limits/page.tsx created + "Usage Limits" sidebar item: shows used/limit and when each slot frees up for every rate-limited feature
  • •no new DB columns, no new env vars, no new npm packages
Jul 12, 2026
Fix

Mobile notice leak, filter cleanup, honest Adzuna label, connection requests rewritten

  • •app/(dashboard)/layout.tsx + app/dashboard/layout.tsx + globals.css: the desktop-only notice is now hidden inline (style="display:none") and only forced visible by the phone media query with !important - it can never render on desktop again, even with a stale stylesheet
  • •app/(dashboard)/jobs/page.tsx: Min salary and Posted filters removed entirely (they depended on data most stored jobs did not have); the filter bar is Location + Job type + saved presets; applying an old preset ignores the removed fields
  • •app/(dashboard)/jobs/page.tsx: job card link relabelled "View on Adzuna" - Adzuna does not expose the employer URL in its API and blocks server-side resolution, so the Adzuna page is the supported destination
  • •app/api/generate-outreach/route.ts: connection requests are now genuine "asking to connect" messages - identity, one conversational line about current work (numbers/percentages and resume phrasing banned), and a connect-only ask; job/referral asks are reserved for cold emails, which keep the pitch structure
  • •no new DB columns, no new env vars, no new npm packages
Jul 11, 2026
Improvement

Daily tier-aware rate limiting across every AI and external endpoint; Groq references retired

  • •lib/rateLimit.ts: checkRateLimit reworked to a tier-aware daily limiter - RateLimitConfig now takes { key, userId, freeLimit, proLimit, windowMinutes }; resolves the user's plan from subscriptions (pro+active only) when ENABLE_FREEMIUM=true, otherwise always applies freeLimit; returns { allowed, remaining, resetAt, showUpgrade }; rateLimitResponse now emits retryAfter (seconds) and freemium-aware copy ("Daily limit reached. Try again tomorrow." vs "...Upgrade to Pro for higher limits."); added checkSharedQuota/incrementSharedQuota for platform-wide caps plus checkJinaQuota/incrementJinaQuota wrappers
  • •lib/rateLimitMessage.ts created: client-safe rateLimitMessage(retryAfter) helper for consistent 429 copy across the app
  • •app/api routes migrated to the tier-aware limiter (freeLimit/proLimit, windowMinutes 1440): tailor-resume (1/5), ats-score (2/7), generate-cover-letter (1/4), parse-job (5/12), parse-resume (1/1), parse-profile (1/1), interview-prep/generate (1/5), interview-prep/assess (5/15), generate-outreach (2/7), discover-jobs (1/1), extension/ai-fill (2/10), and newly-limited answer-question (5/20), analyze-form (2/10), example (5/10), strategy-feedback (1/3 at generation)
  • •app/api/find-contacts/route.ts: homegrown route_logs day counter replaced with checkRateLimit (1/1) + shared SerpApi budget checkSharedQuota('serpapi_monthly', 250, 30); incrementSharedQuota fires only after a successful SerpApi response
  • •app/api/parse-job + analyze-form: shared Jina budget checkSharedQuota('jina_lifetime', 150, 36500) guards URL scraping (pasted text never charged); incrementSharedQuota after a successful scrape
  • •frontend 429 handling wired into tailor (via apiFetch), ats, interview, networking, jobs pages plus ProfileForm, ResumeUpload, apply page, and extension/popup.js - 429s now render friendly "try again in X" copy, not raw JSON
  • •app/(marketing)/privacy/page.tsx: AI inference vendor section updated from Groq to Google (Gemini)
  • •new DB table: platform_limits (key, count, window_start) with RLS deny-all + increment_platform_limit(p_key) SQL function - see supabase/migrations/20260711_platform_limits.sql (run it if the table is missing; rate_limit_logs already exists)
  • •no new env vars, no new npm packages
Jul 11, 2026
Improvement

Three-tier rate limiting (dev/free-monthly/pro-daily), free-tier 150 application cap

  • •lib/rateLimit.ts: RateLimitConfig now carries devLimit/freeLimit/proLimit plus devWindowMinutes/freeWindowMinutes/proWindowMinutes; checkRateLimit resolves a dev|free|pro tier and applies the matching limit+window (dev 1440, free 43200, pro 1440); result returns tier instead of showUpgrade; added exported resolveUserTier(userId)
  • •lib/rateLimit.ts rateLimitResponse(resetAt, remaining, tier): free tier -> "Monthly limit reached. Upgrade to Pro for higher limits or try again next month."; dev/pro -> "Daily limit reached. Try again tomorrow."
  • •every AI/external route migrated to the three-tier config (dev/free-month/pro-day): tailor-resume 1/10/5, ats-score 2/10/7, cover-letter 1/10/4, parse-job 5/20/12, parse-resume 1/3/1, parse-profile 1/3/1, interview generate 1/5/5, interview assess 5/15/15, networking outreach 2/15/7, find-contacts 1/3/1, discover-jobs 1/3/1, extension/ai-fill 2/20/10, plus answer-question 5/15/20, analyze-form 2/10/10, example 5/10/10
  • •app/api/strategy-feedback/route.ts: dev 2/day, free 2/month, pro skipped entirely via resolveUserTier gate
  • •app/api/applications/route.ts POST: free tier capped at 150 tracked applications (count of non-deleted rows) returning 403 with an upgrade message; dev mode and pro users skip the check; removed the old checkLimit('applications') 429 path (GET still reports atLimit)
  • •frontend 429 handlers (jobs, ats, interview, networking, tailor apiFetch, apply, ProfileForm, ResumeUpload, extension/popup.js) now surface the server's tier-aware message (json.error) instead of a locally computed hours string, so monthly limits read correctly
  • •no new DB columns, no new env vars, no new npm packages
Jul 11, 2026
Update

Admin account bypasses all per-user rate limits for testing

  • •lib/rateLimit.ts: new isAdminUser(userId) helper comparing against ADMIN_USER_ID; checkLimit and getRemainingLimits return unlimited for the admin, and checkRateLimit returns allowed immediately (tier 'dev', no rate_limit_logs row written)
  • •shared platform quotas (SerpApi monthly, Jina lifetime) are NOT bypassed - those protect real external API allowances
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Dark-glass visual overhaul - gold removed from buttons/cards/badges/inputs, kept only as a restrained accent

  • •app/globals.css: new tokens --btn-primary-bg/--btn-primary-bg-hover/--btn-primary-text/--glass-hover for dark-filled white-text primary buttons. Rewrote .ds-card/.dashboard-card/.dash-card-row/.lp-feature-card/.lp-pain-card/.glass-rim/.glass-rim-strong/.lp-window to drop borders and gold box-shadow glows, relying on background step-up (--card to --card-raised) plus shadow for depth. Added .ds-btn (dark-glass secondary button), .ds-avatar, .ds-icon-tile, .filter-pill (dark-glass toggle/filter pill with a neutral inset-highlight active state) helper classes. .status-badge, .dashboard-document-label, .lp-badge-gold/.lp-badge-green, .lp-autofill-tag recompacted to the pill spec (padding 2px 7px, font-size 10px, border-radius 20px, no border). .dashboard-stat-label/.lp-mock-stat-label gap tightened to 2px. .filter-control and CustomSelect's default input path now sit on --card-raised with a --border-subtle hairline instead of --border. Input focus rings (.dashboard-main input:focus, .input-gold-focus) switched from gold to neutral --focus-ring. Landing eyebrows (.ds-section-label, .lp-feature-num, EYEBROW/CTA_OUTLINE consts) shrunk to 10px muted instead of gold
  • •components/Sidebar.tsx, components/ui/ScoreBadge.tsx, components/CustomSelect.tsx, components/ApplicationDetailModal.tsx: avatars/badges/inputs/dropdowns updated to the same dark-glass + compact-pill treatment (no gold borders, no colored card borders)
  • •app/(marketing)/page.tsx: CTA_GOLD border dimmed from a solid gold line to a low-opacity tint (fill still carries the restrained gold accent); CTA_OUTLINE, ICON_TILE, pricing/step cards, and the closing-CTA badges converted to dark glass / compact pills; the marketing headline gold accent is unchanged (the one deliberate gold moment on the page)
  • •app/(dashboard)/{apply,ats,billing,interview,jobs,networking,tailor,tracker}/page.tsx, apply/[jobId]/page.tsx, strategy/StrategyReport.tsx: removed gold-filled/gold-bordered primary buttons, icon tiles, filter tabs, and card borders in favor of the dark-filled-white-text primary button and borderless card system; the "applied" status badge is the one place gold remains, as the existing semantic status color
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Landing hero rebuilt as an overlay composition - metallic headline in front of a dimmed workspace with a studio-light beam

  • •app/(marketing)/page.tsx: replaced the split-grid hero (headline left, tilted app window right) with a full-bleed aria-hidden backdrop stage of the actual product: ghosted sidebar with the real nav labels, a Tailor + ATS panel (overall score 87, keyword/format/experience/skills subscore bars matching the ats_scores columns, and a per-bullet before/after diff with a lit "After Vantage" line), and an Applications panel reusing TrackerRow with 7 rows (added Anthropic/Snowflake/Duolingo). Headline/CTAs now float in front with metallic gradient text (.lph-metal silver, .lph-metal-gold for "There's a better way.")
  • •app/globals.css: removed .lp-hero/.lp-hero-grid/.lp-hero-float; added the .lph-* hero system (stage, mock grid, panels, subscore bars, diff, beam, shade, metallic text). New :root/light tokens: --hero-ink-strong/--hero-ink-soft (radial ink shade that darkens the mockup behind the copy), --hero-beam/--hero-beam-core (diagonal blurred studio-light beam, fades in once, disabled under prefers-reduced-motion), --metal-a..d and --metal-gold-a..d gradient stops for background-clip headline text in both themes
  • •responsive: right panel hides under 1100px; under 860px the sidebar hides, the mock drops to a faint single-panel backdrop, and the shade strengthens so copy stays readable at 375px
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Hero mockup unified into one app screen with the headline bottom-left

  • •app/(marketing)/page.tsx: the hero backdrop is now a single Tailor + ATS screen instead of separate floating panels: sidebar with wordmark and real nav labels, page header row with a job-context chip, ATS score card (87, subscore bars, missing-keyword pills), a bullet-by-bullet changes card with three before/after diff rows (first row lit), and a cropped keyword-coverage table (keyword / mentions in job / mentions in resume / covered-missing status) spanning the bottom. Removed the HERO_ROWS applications panel; added DIFF_ROWS, MISSING_KEYWORDS, COVERAGE_ROWS mock data
  • •app/globals.css: hero copy now anchors bottom-left (align-items flex-end) matching the reference layout; .lph-panel/.lph-main/.lph-rightcol replaced by .lph-body/.lph-pagehead/.lph-chip/.lph-content/.lph-card/.lph-kws/.lph-diff-row/.lph-trow; cards flattened (shadow-sm, sit in the screen instead of floating); sidebar gains a hairline right border so the mock reads as one canvas; new --hero-veil token adds a uniform dim layer over the whole mockup so only the lit diff bullet stays bright (non-lit diff lines dropped to muted); shade/beam repositioned for the bottom-left copy
  • •responsive: coverage table hides under 1100px; under 860px the sidebar and score card hide and the diff card ghosts behind strengthened shading
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Hero lighting reworked to match the reference - diagonal shaft from the header, dark corners

  • •app/globals.css: the hero shade now models one light source entering at the top (left of center, appearing to come from under the header) and falling at 45 degrees toward bottom-right: a diagonal transparent corridor in the veil reveals the app example along the light path, a distance-falloff radial dims it before bottom-right, a bottom-right radial pool makes that corner almost black so nothing competes with the hero text, and the top-right end of the corridor gradient keeps that corner dim; the top fade shrank (14% to 7%) so the light touches the header edge
  • •app/globals.css: .lph-beam realigned to the corridor (left 41%, rotate -45deg, wider, brighter core via --hero-beam/--hero-beam-core) with a mask-image fade so the visible shaft dies out along its length; a comment notes the beam and the 45deg corridor in .lph-shade must stay in sync
  • •verified by pixel sampling: shaft path reads ~3x brighter than the dark zones; coverage-table right, bottom-right, and text zones sit at near-background black in dark mode
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Landing cards get a metallic-glass treatment - light diffuses inward from the border

  • •app/globals.css: new .glass-card class (landing only for now): the top edge light diffuses INTO the surface as a blurred inset bloom instead of a crisp 1px line, the fill carries a faint top-left sheen (same light direction as the hero beam), a hairline glass edge wraps the card, the outer drop shadow is tight and soft, and a shared ::after adds subtle SVG fractal-noise grain (mix-blend overlay, dimmer in light mode). Light-mode variant uses a white sheen, ink hairline, and soft drop
  • •app/globals.css: .glass-rim-strong (Pro pricing card, closing CTA band) upgraded to the same recipe with a stronger sheen and bloom on --card-raised; .lp-window (feature mockup frames) and .lph-card (hero mockup cards) get the sheen + inner bloom + hairline; .lp-pain-card now composes with .glass-card and its hover brightens the inner bloom instead of deepening an outer shadow
  • •app/(marketing)/page.tsx: pain cards, how-it-works step cards, and the Free pricing card now use the glass-card class instead of inline background/box-shadow styles
  • •pending user review: if approved, this treatment rolls out to every other page in the app
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Bespoke feature artifacts and a premium closing CTA

  • •app/(marketing)/page.tsx: the three feature visuals are no longer three identical app windows. 01 Resume tailoring is now a layered "slips" artifact on a glass stage: a dim struck-through Before slip, a small swap chip, and a floating lit After slip with the keyword badge. 03 Auto-apply is a mini application form (name, email, work authorization, drafted answer) with green checks and an overlapping extension toast reading "Form filled from your profile / Review and submit when ready." 02 Application tracking keeps the app window as the one true app screen. Removed JOB_ROWS and the unused .lp-match/.lp-autofill-tag styles; added FORM_ROWS
  • •app/(marketing)/page.tsx: closing CTA band upgraded: headline set in the metallic gradient (.lph-metal), the before/after box replaced with the same floating slips, and a new lp-cta-special class adds a faint warm gold glow from the top-left plus a white counter-glow (the closing CTA is one of the allowed gold moments)
  • •app/globals.css: new .lp-stage, .lp-slip/.lp-slip-before/.lp-slip-after, .lp-swap-chip, .lp-mini-form/.lp-form-row/.lp-form-label/.lp-form-value/.lp-form-check, .lp-ext-toast*, .lp-cta-special classes, all with light-mode variants, following the metallic-glass recipe (inset bloom, top-left sheen, hairline edge, soft drop)
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Landing page containers widened to the hero mockup's line

  • •all left-aligned landing containers now share the hero mockup's edge (max-width 1400px, padding clamp(24px, 4vw, 60px)): hero copy (.lph-copy), sound-familiar / how-it-works / pricing / closing-CTA sections (app/(marketing)/page.tsx inline styles), .lp-features, and .lp-support in globals.css
  • •components/marketing/MarketingNav.tsx and MarketingFooter.tsx widened to the same frame so the wordmark, page content, and footer sit on one line
  • •centered content (the 780px pricing card pair, centered section headings) left as is
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Hero lighting tuned, metallic gold CTA button, Pro card elevated

  • •app/globals.css: hero ink pool behind the headline enlarged and strengthened (the mockup text no longer ghosts through the headline), the light corridor widened toward the upper right (transparent band 42-64 percent), the distance falloff relaxed, and the beam made wider (clamp 260px-500px), re-centered (left 42 percent), and extended further down its path (mask fade 46-86 percent)
  • •app/globals.css: new .lp-metal-btn - metallic gold pill button built from the --metal-gold-* gradient stops (lighter top, darker base, top glint, soft gold-tinted drop); dark text in dark mode, white text on the deeper light-mode golds. Used on the closing CTA and the Pro pricing card CTA; Free card keeps the dim gold link
  • •app/globals.css: .lp-slip-after brightened to frosted glass (stronger sheen and bloom) and .lp-cta-special::before gains a diagonal brushed-metal sheen plus a stronger gold ambient glow
  • •app/globals.css: new .lp-pro-card - the Pro pricing card lifts 8px, takes a brighter sheen, stronger inner bloom, and a deeper drop shadow than the Free card (lift disabled on mobile single-column)
  • •app/(marketing)/page.tsx: closing CTA and Pro card buttons switched to lp-metal-btn; Pro card gets the lp-pro-card class
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Landing polish - features header animation, macOS window dots, real autofill data, level pricing cards, CTA details

  • •app/(marketing)/page.tsx: "Your unfair advantage / Three things that change your odds" header now wrapped in Reveal so it fades in like the rest of the page; auto-apply mock uses Ishvir Chopra / ischopra@uwaterloo.ca and the drafted answer is wrapped in square brackets to read as a placeholder; closing CTA eyebrow gets a small gold kicker tick; CTA_OUTLINE and ICON_TILE gain a subtle top glint
  • •app/globals.css: .lp-window-dot colored as macOS traffic lights (red/amber/green) with a tiny glint; .lp-pro-card no longer lifts upward (stays level with the Free card, still brighter with the metal button); .lp-cta-special gains a brighter top glint and deeper drop shadow; .lp-mock-logo tiles get an inset glint
  • •per user instruction, no new background gradients were added - all premium detail comes from inset glints, rims, and shadows
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Feature

Closing CTA becomes live metal - cursor-tracked lighting

  • •components/marketing/CtaSpotlight.tsx (new, client component): wraps the closing CTA card, tracks the pointer, and writes --mx/--my CSS variables directly on the element (no React state, cheap mousemove). Touch devices never set the variables and keep the static lighting
  • •app/globals.css: .lp-cta-light spotlight layer (tight specular + wide halo) follows the cursor and fades in via .is-lit; .lp-cta-metal-text re-angles the headline's metal gradient and slides its highlight band with --mx so the lettering re-lights toward the pointer like engraved metal; .lp-btn-glint sweeps a skewed specular stripe across the gold button every 5.5s (the one automatic motion, so mobile gets the moment). All of it is disabled under prefers-reduced-motion; light mode uses a warm gold spotlight
  • •app/(marketing)/page.tsx: CTA card wrapped in CtaSpotlight; headline span gains lp-cta-metal-text; button gains lp-btn-glint
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Cursor light on all cards and mockups; closing CTA composition upgraded

  • •components/marketing/Spotlight.tsx (new, replaces CtaSpotlight.tsx): generic cursor-tracked light wrapper with an optional ariaHidden prop for decorative mockups; renders the .lp-hover-light layer and writes --mx/--my directly on the element
  • •app/(marketing)/page.tsx: Spotlight now wraps the feature mockup stages (tailoring slips, auto-apply form), the AppWindow frames, the how-it-works step cards, and both pricing cards. The pain-quote cards are deliberately excluded per user instruction; the hero backdrop is untouched (pointer-events none)
  • •app/globals.css: .lp-cta-light generalized to .lp-hover-light (quiet version for ordinary cards, stronger override on .lp-cta-special; gold-tinted in light mode)
  • •closing CTA composition: padding up to clamp(40px, 5.5vw, 68px), headline up to clamp(30px, 4vw, 46px) at weight 800, microcopy ("Free during early access - No credit card required") under the metal button, the before/after slips now bridged by the swap chip like the feature-01 artifact, and a giant ghost "Vantage" wordmark (.lp-cta-ghost, opacity 0.04) anchored bottom-right behind the content, echoing the step cards' ghost numerals
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Metallic-glass rollout across the dashboard, resources, company, and legal pages

  • •app/globals.css: the shared dashboard surfaces (.ds-card, .dashboard-card, .dash-card-row and its hover) upgraded to the metallic-glass recipe - top-left sheen, inset top bloom, hairline glass edge, tight soft drop - with light-mode variants. Paint-only changes (no position or pseudo-element additions) so no dashboard layout can shift. Blog and changelog cards inherit automatically via .ds-card
  • •app/globals.css: .ds-btn gains a top glint (hover keeps it), .ds-btn-primary and .dashboard-primary-button get a machined top-glint plus bottom-shade pair, .ds-icon-tile gets a glint
  • •app/(marketing)/docs/page.tsx: topic cards wrapped in Spotlight (cursor light) with the link filling the card; all 11 docs subpage titles set in the metallic gradient via a scripted h1 wrap
  • •app/(marketing)/{blog,changelog,about,contact,privacy,terms}/page.tsx: page titles set in the metallic gradient (.lph-metal); blog featured post card wrapped in Spotlight
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Light-mode hero slash goes gold; Pro pricing card differentiated in light mode

  • •app/globals.css: light-theme --hero-beam/--hero-beam-core switched from a dark glare to subtle gold (rgba(201,162,39) at 0.09/0.18) so the hero light shaft reads as warm sunlight in light mode; dark mode beam unchanged
  • •app/globals.css: light-theme .lp-pro-card now sits on bright white (--card, was the grayer --card-raised) with a faint warm sheen, a low-opacity gold inset hairline (CTA_GOLD-style), and a warm-tinted deep drop shadow; dark-mode Pro card untouched
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Dark mode is the default; Pro card becomes a dark island in light mode; docs/blog cursor light removed

  • •app/layout.tsx: default theme is now dark - both the SSR data-theme attribute and the theme script fallback (localStorage "vantage-theme" still wins for users who chose a theme)
  • •app/globals.css: in light mode the Pro pricing card is now a dark-glass island embedded in the light page - theme tokens (--text/--muted/--gold-dim/--metal-gold-*) are re-scoped locally on .glass-rim-strong.lp-pro-card so all content inverts, on a #131313 glass surface with full-strength grain and the dark-mode metal button face; the extra class in the selector beats the later .glass-rim-strong light rule that was silently overriding the background. Dark-mode Pro card untouched
  • •app/(marketing)/docs/page.tsx and blog/page.tsx: cursor Spotlight removed per user request - docs topic cards and the blog featured card are plain glass link cards again
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Feature

Feedback page, changelog timeline, custom scrollbars, external-link icon, wider reading pages, gilded Pro card

  • •app/(marketing)/feedback/page.tsx + FeedbackForm.tsx (new): public "Help make Vantage better" page - bug report / feature request toggle, summary, details, optional email; controlled inputs with onClick submit (no form tag), disabled button with Spinner while sending, inline success and error states. Linked from the footer Resources column
  • •app/api/feedback/route.ts (new): public POST endpoint that validates the submission and forwards it as a JSON row to FEEDBACK_SHEET_WEBHOOK_URL (point it at a Power Automate "HTTP request received - Add a row into an Excel table" flow or any sheet-webhook service); returns a clear 503 message if the env var is not set
  • •new env var: FEEDBACK_SHEET_WEBHOOK_URL (server only)
  • •app/(marketing)/changelog/page.tsx + app/globals.css: changelog rebuilt as a timeline (.cl-*) - date and type pill on a left rail, a continuous line with a node per entry (newest node bright), entries as clean content blocks instead of stacked cards; single-column collapse under 700px; page widened to 980px
  • •components/ui/ExternalLinkIcon.tsx (new): square-with-arrow external-link icon; replaced the literal arrow character in jobs (View job) and networking (View Profile, contact-name links)
  • •app/globals.css: custom scrollbars app-wide - thin rounded thumb on a transparent track via new --scrollbar-thumb/--scrollbar-thumb-hover tokens (both themes), webkit + Firefox
  • •reading pages widened: terms and privacy 680 to 860, about 640 to 820, contact 560 to 760, blog posts 680 to 860
  • •app/globals.css: light-mode Pro pricing card is no longer a dark island - it is gilded: warm ivory surface, metallic gold edge along the top, warm gold hairline and warm deep shadow; stays light, still clearly featured. Dark mode unchanged
  • •no new DB columns, no new npm packages
Jul 10, 2026
Update

Contact merged into feedback, signup agreements, changelog jump list, Pro card gold line, footer links

  • •app/(auth)/signup/page.tsx: signup now requires agreeing to the Terms of Service and Privacy Policy (checkbox, validated before submit) and offers an optional marketing-emails opt-in. Both are stored in auth user metadata (marketing_opt_in, terms_accepted_at) as boilerplate for the future mailing-list feature - no emails are sent yet
  • •contact page merged into /feedback: app/(marketing)/contact/page.tsx now redirects to /feedback; components/marketing/ContactForm.tsx and app/api/contact deleted. The feedback form subject is a CustomSelect with Bug report / Feature request / General question / Partnership / Press / Other (placeholders adapt per subject); /api/feedback accepts the new types and forwards the label to the sheet webhook. Feedback page gains a "Prefer email?" fallback line
  • •links updated: footer Company column now About + Feedback (Contact removed, Feedback out of Resources); dashboard sidebar footer Contact links point to /feedback; terms and about pages reference the feedback page
  • •components/marketing/MarketingFooter.tsx: GitHub (github.com/Ishvirchopra35) and portfolio (ishvirschopra35.tech) icon links added to the social row
  • •app/globals.css: Pro pricing card simplified to a single 3px metallic gold line along the top edge in BOTH themes - light mode drops the ivory wash, sheen, gold hairline, and warm glow; dark mode otherwise unchanged
  • •app/(marketing)/changelog/page.tsx + app/globals.css: "Jump to an update" disclosure under the header - a two-column anchor list (date + title) linking to #update-N ids on each timeline entry, with scroll-margin so anchors land clear of the nav
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Glass sidebar highlight, connection-code copy cleanup, tailor toggle arrow

  • •components/Sidebar.tsx: the active nav item is no longer a gold-tinted bar - it is glass (raised surface, top glint, themed hairline via --glass-hover), consistent with the dark-glass system
  • •app/(dashboard)/apply/[jobId]/page.tsx: removed the "Manage connection code" link from the per-application auto-fill card (the main auto-apply setup page keeps its code management); the "Already installed?" link now says the code lives at the bottom of the Profile page
  • •app/(dashboard)/apply/page.tsx: setup step 2 copy points to the bottom of the Profile page and the button links to /profile#extension-token
  • •app/(dashboard)/profile/page.tsx: added the extension-token anchor (with scroll margin) around ExtensionTokenSection so those links actually scroll to it
  • •app/(dashboard)/tailor/page.tsx: "URL not working? Paste the description directly" now ends with the right ArrowIcon
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Fix

Sidebar highlight keeps its gold bar and tint with a glass fill; extension toast uses the real logo

  • •components/Sidebar.tsx: active nav item restored to the gold left bar + gold-dim tint, with the fill itself now glass (top-left sheen layer over the tint, top glint, themed hairline)
  • •app/(marketing)/page.tsx + app/globals.css: the auto-apply mockup extension toast shows the Vantage logo image instead of a "V" letter tile
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Extension popup restyled to the dark-glass system; SEO hardening

  • •extension/popup.html: popup stylesheet rewritten to the current design system - gold updated to #c9a227 and confined to the status badge (compact pill, no border); primary button is dark-filled white text with the machined glint; secondary/icon buttons are dark glass with inset highlights; cards are borderless glass with sheen, glint, hairline, and soft drop; inputs sit on card-raised with the neutral focus ring; result boxes use tinted inset hairlines for success/error; thin custom scrollbar; hint links are white with underline instead of gold
  • •app/layout.tsx: title template ("%s - Vantage" on every child page), keywords, publisher/category, explicit robots directives (index/follow, large image preview), author URL
  • •app/(marketing)/page.tsx: JSON-LD graph gains a WebSite node and sameAs profiles (GitHub, LinkedIn, Instagram, YouTube, portfolio) on the Organization and founder for entity recognition
  • •app/(marketing)/blog/[slug]/page.tsx: BlogPosting JSON-LD on every post
  • •app/sitemap.ts: /contact replaced with /feedback
  • •no new DB columns, no new env vars, no new npm packages
Jul 10, 2026
Update

Google Search Console verification tag wired to an env var

  • •app/layout.tsx: metadata.verification.google reads NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION; the meta tag is omitted when the variable is unset
  • •new env var: NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION (optional)
Jul 8, 2026
Improvement

Plain-language pass across all user-facing copy

  • •replaced jargon in visible strings app-wide: "parse" to "read"/"analyze", "fetch" to "find"/"load" (tailor, jobs, ats, apply, profile, privacy, docs, blog)
  • •"token" no longer shown to users: the extension pairing secret is now called a "connection code" everywhere (components/ExtensionTokenSection.tsx, docs/extension, docs/auto-fill, apply/[jobId] links); variable and DB names (extension_token) unchanged
  • •tailor page: "Parse job" button is now "Analyze job"; jobs page: "Fetch New"/"Fetch Jobs" now "Find New"/"Find Jobs", "Fetching..." now "Finding..."
  • •ATS now gets a brief plain-language clarification on first mention per page (tailor, ats, dashboard, apply detail, strategy report, landing, privacy, and the getting-started/resume-tailoring/auto-fill/extension/strategy-feedback docs)
  • •removed em dashes from all live user-facing text: app/layout.tsx + (marketing)/layout.tsx + (marketing)/page.tsx titles now use a hyphen; landing quote attribution dash removed
  • •error copy moved to active voice ("We could not read this job posting.", "We could not load jobs right now.")
  • •note: the append-only changelog, SEO meta keyword strings, and terse compact data labels (tracker stat tiles/columns) were intentionally left unchanged
Jul 8, 2026
Feature

Blog post - The New Vantage design overhaul

  • •content/blog/the-new-vantage-design-overhaul.mdx: News-category post announcing the design system v2 overhaul (gold accent, Outfit/Manrope type, redesigned landing + dashboard, per-bullet tailoring diff, plain-language pass)
  • •appears as the featured post on /blog and is picked up automatically by generateStaticParams and the sitemap
Jul 8, 2026
Improvement

Subtle gold particle background on the landing page

  • •components/marketing/Particles.tsx (new): canvas particle field with mouse magnetism, alpha fade near edges, dpr-aware sizing; self-contained (no external cn/util dep), transparent background, pointer-events-none and aria-hidden so it never intercepts clicks or reaches assistive tech
  • •components/marketing/LandingParticles.tsx (new): theme-aware wrapper that feeds a variation of --gold to the field — brighter gold (#e0bd6a) on the dark #0a0a0a bg, deeper amber-gold (#b8862f) on the light #fafafa bg — and swaps live via a MutationObserver on the html data-theme attribute
  • •app/(marketing)/page.tsx: landing content wrapped in a relative container with the particles absolutely positioned behind (z-0) and content above (z-1); scoped to the landing page only, so blog/docs/other marketing pages are unaffected. Placed inside a relative wrapper rather than a fixed -z-1 layer because html/body both paint an opaque var(--bg)
Jul 8, 2026
Improvement

Unified page letterhead across every dashboard page

  • •components/ui/PageHeader.tsx (new): one shared header for every dashboard page - a gold signature tick, an Outfit title (26px, tighter tracking), an optional muted subtitle, and an optional right-aligned action slot. Presentational only, so it works in both server and client pages
  • •app/globals.css: added .page-header / .page-header-kicker / .page-header-action classes, including the mobile stack where the action becomes a full-width tap target
  • •replaced the ad-hoc, incrementally-built headers on every page with PageHeader: dashboard home, jobs, tracker (Applications), tailor, ats, strategy (both locked and report states), networking, interview, apply, billing, documents, settings, profile. Headers were previously a mix of div/h1, 24px, and 4/8/24/28px margins
  • •ats page now has a real page title ("ATS scores") for the first time; its score list keeps a quiet "Score history" section label. Settings dropped its stray in-card back button (navigation is the sidebar)
  • •copy tidied to sentence case where the header changed: "Job feed", "Find new", "Interview practice"
Jul 8, 2026
Improvement

One answer-method control per interview session instead of per card

  • •app/(dashboard)/interview/page.tsx: the Voice/Type toggle used to sit inside every question card, crowding each one. Replaced it with a single segmented "Answer with" control in the session toolbar (next to the category tabs); every card now follows that one selection
  • •answerMode state changed from a per-question Record to a single session-wide AnswerMode; browsers without speech recognition auto-default to Type and the Voice segment is disabled
  • •each card keeps its own "Hear question" button; the crowded hear + voice + type button cluster is gone
Jul 8, 2026
Improvement

Auto-apply page now leads with Chrome extension setup

  • •app/(dashboard)/apply/page.tsx: added a "Set up once" card with a two-step checklist - install the Chrome extension, then connect it with a connection code - above the job selection list, reframed as "Then apply / Choose an application"
  • •step 2 reflects real state: it reads profiles.extension_token in parallel with the applications query and shows a green check when a connection code already exists (links to Profile to manage it, otherwise to generate one)
  • •the extension is not on the Chrome Web Store yet, so step 1's "Get the extension" button uses a CHROME_EXTENSION_URL placeholder constant pointing at the /docs/extension install guide (no dead href="#"); swap the constant for the store URL when the extension ships
  • •applications query parallelized with the new profile lookup via Promise.all
Jul 8, 2026
Fix

Documentation accuracy pass across all docs pages

  • •audited every /docs page against the current app and corrected drift from features that changed
  • •auto-fill: rewrote to match the real flow (Auto-apply page + Chrome extension fill + the Application Questions answerer); removed the "Apply Prep > Console tab / Copy-paste tab" steps for console-snippet and answers-panel UIs that no longer exist
  • •interview-prep: third question category is Role-specific (was wrongly "Company-specific"); answer method is now one session-level "Answer with Voice/Type" control, not a per-question choice; assessment described as Content + Delivery scores with filler-word detection
  • •ats-scoring: scoring happens in the Tailor flow (ATS Score tab); the ATS page is the read-only score history, not a place you paste a job
  • •resume-tailoring: steps now match the real buttons (Analyze job -> Tailor My Resume) and tabbed results
  • •application-tracking: button is "Log application" (not "Add application"); page is "Applications"
  • •cover-letters: generated from the Tailor flow's Cover Letter tab and saved to Documents
  • •extension: noted the Chrome Web Store listing is not live yet (developer-mode install for now); fixed the troubleshooting step that pointed at the removed console-snippet tab; removed the nonexistent "direct link from your Profile page"
  • •networking: contact search targets recruiters/hiring managers via public LinkedIn search and is capped per day
  • •getting-started: tailor and log-application steps corrected; bumped "Last updated" to July 8, 2026 on every edited page (billing was already accurate)
Jul 8, 2026
Fix

Stop showing raw errors to users; log them to console.error instead

  • •lib/apiResponse.ts: serverError() no longer returns the raw exception message to the client. It logs the real error with console.error and returns a generic "Something went wrong on our end. Please try again." This fixes raw-error exposure across all 49 API routes that use it in their catch blocks
  • •app/(auth)/login + signup: Supabase auth errors are logged and mapped to friendly copy ("Incorrect email or password.", "An account with this email already exists.") instead of surfacing the raw message
  • •app/(dashboard)/interview: three spots (tracker load, assess, start session) that showed the raw error/e.message now log it and show a friendly message
  • •app/(dashboard)/ats: raw Supabase error messages on load replaced with a friendly message + console.error
  • •app/(dashboard)/actions.ts (updateProfile): upsert and catch now log the raw error and return a friendly message, so ProfileForm no longer displays raw DB errors
  • •components/ResumeUpload.tsx: upload catch shows a friendly message and logs the raw error
  • •app/error.tsx: the global error boundary now logs the error via console.error (the raw message it displays was already gated to development only)
Jul 8, 2026
Fix

Logo now legible in light mode

  • •app/globals.css: the logo mark is a light silver PNG with no dark-background variant, so it blended into the light theme. Added a light-mode-only CSS filter (brightness(0.45) contrast(1.2)) on img[src="/logo.png"] that shifts it to a dark graphite tone while keeping the metallic gradient. Dark mode is unchanged; no JSX edits needed since the rule targets the logo images directly
Jul 8, 2026
Update

Light mode is now the default theme

  • •app/layout.tsx: the SSR <html data-theme> and the pre-hydration theme script now default to "light" instead of "dark" when no preference is stored
  • •components/Sidebar.tsx + components/SettingsClient.tsx: theme toggles initialize to "light" as the fallback
  • •components/marketing/LandingParticles.tsx: particle color falls back to the light-mode gold before hydration
  • •returning users who previously picked a theme keep their choice (localStorage vantage-theme is unchanged)
Jul 7, 2026
Improvement

Design system v2 - Outfit/Manrope fonts, gold accent, elevation tokens

  • •app/layout.tsx: Google Fonts link tags in head (preconnect + stylesheet) loading Outfit 600/700/800 (display) and Manrope 400/500/600 (body)
  • •app/globals.css: new tokens merged into the existing :root and light-theme blocks (all existing variables and app styles preserved) - --card-raised, --card-sunken, --border-subtle, --gold, --gold-dim, --gold-border, --radius-sm (8px), --radius-lg (16px), --shadow-sm/md/lg/glow, --font-display (Outfit), --font-body (Manrope), each with light-mode variants where they differ; html/body font-family switched to var(--font-body) with the old system stack as fallback, body gains line-height 1.5 and antialiasing; new h1-h6 rule with var(--font-display), weight 700, letter-spacing -0.02em
  • •components/Sidebar.tsx (styles only, desktop + mobile drawer): sidebar root uses var(--font-body); active nav item gets var(--card-raised) background and var(--shadow-sm) on top of the existing accent left border; nav items get transition all 0.15s ease and var(--radius-sm); sidebar/topbar/drawer borders use var(--border-subtle); avatar gets 2px solid var(--gold-border); rectangular border-radius values (4/5/8/10px) standardized to var(--radius-sm) - circular elements (avatar, Pro/Free pills, close button) intentionally kept round
Jul 7, 2026
Improvement

Sidebar nav hover state

  • •components/Sidebar.tsx: NavLink tracks a hovered state via useState with onMouseEnter/onMouseLeave on the link; non-active items show background var(--card-raised) on hover (fades via the existing 0.15s transition), active items unchanged (they already have the raised background plus shadow); coming-soon placeholders stay inert
Jul 7, 2026
Improvement

Landing page redesign - gold signature treatment, Outfit/Manrope type system

  • •components/marketing/MarketingNav.tsx: dark/light toggle removed entirely (theme state, Sun/Moon icons, drawer toggle all deleted - marketing pages now render dark); navbar background rgba(10,10,10,0.8) with backdrop blur(12px) and var(--border) bottom border; wordmark in Outfit 700 at 1.35rem; "Get started" CTA now the signature gold treatment (var(--gold) border, var(--gold-dim) background, gold text, var(--radius)); nav links restyled via .mkt-nav-link
  • •app/(marketing)/page.tsx: hero pill badge replaced with gold "BUILT FOR RECRUITING SEASON" eyebrow (Outfit 500 12px uppercase); headline Outfit 800 clamp(48px, 8vw, 96px) letter-spacing -0.04em with "There's a better way." in gold on its own line; subheadline Manrope 16px max-width 480px; gold primary + outlined secondary CTAs (12px radius, 14px Manrope 600, 12px 24px padding); microcopy updated to "Free during early access"; all section eyebrows unified to gold Outfit 600 11px uppercase; pain-point cards get var(--shadow-md) + var(--radius); feature cards use var(--card-raised) + var(--shadow-md) with hover to var(--gold-border) border + var(--shadow-glow) (0.2s ease), 36px var(--gold-dim) icon tiles with gold icons; how-it-works steps get a large Outfit 800 gold watermark number at 0.15 opacity behind relative content, var(--card) background, 2px var(--gold-border) top border, new per-step icons; pricing Pro card featured with var(--card-raised) + var(--gold-border) + var(--shadow-glow) and gold-dim "Most popular" badge, prices Outfit 800 42px, checkmarks gold for Pro / var(--score-green) for Free, CTAs gold; removed the redundant fixed background div
  • •components/marketing/MarketingFooter.tsx: background var(--bg) (was var(--card)), top border var(--border-subtle), root font Manrope, copyright/tagline 13px, wordmark Outfit 700, social link colors moved from hardcoded hex to var(--muted)/var(--text)
  • •app/globals.css: stale unused .lp-* landing classes replaced with .lp-grid-3/.lp-grid-4/.lp-pricing-grid (responsive: 4-col to 2-col to 1-col) and .lp-feature-card with the gold hover state; .mkt-nav-link restyled to Manrope 14px var(--muted) uppercase letter-spacing 0.06em with color-based hover/active; .mkt-footer-link pinned to Manrope 13px
Jul 7, 2026
Improvement

Landing page density + gold visibility pass

  • •app/(marketing)/page.tsx: section vertical padding cut ~40% (hero 96px -> 56px, mid sections 90px -> 54px, closing CTA 80/96px -> 48/56px) for a denser page
  • •app/globals.css: dark-mode --gold-border strengthened from 0.25 to 0.4 alpha and --shadow-glow's outer glow from 4px 24px at 0.08 to 6px 32px at 0.18 - at the old values the gold ring and glow were imperceptible on the #0a0a0a background, which made feature-card hover and the Pro pricing card read flat (the styles themselves were already wired: shadow-md default on feature cards, card-raised/gold-border/glow/gold checkmarks on Pro); everything referencing the tokens (feature hover, Pro card, how-it-works top borders, sidebar avatar ring) gets the stronger treatment automatically
  • •app/globals.css: .lp-feature-card transition narrowed from all to box-shadow + border-color (0.2s ease)
Jul 7, 2026
Improvement

Landing hero eyebrow re-specced, pain-point cards raised

  • •app/(marketing)/page.tsx: hero eyebrow set to exact spec (gold, Outfit 500, 11px uppercase, 0.1em tracking, 16px bottom margin - was 12px/0.12em/20px); hero section gains paddingTop 80 so it clears the navbar; "Sound familiar?" cards moved to var(--card-raised) background with padding 20, fontSize 15, lineHeight 1.6, color var(--text)
Jul 7, 2026
Improvement

Dashboard design overhaul - Outfit/Manrope type, gold signature, new Auto-apply page

  • •components/Sidebar.tsx: nav items now 40px tall with 0 12px padding and var(--radius-sm); active item gets var(--card-raised) background + 2px var(--gold) left border + 600 weight (transparent left border on inactive prevents layout shift); hover shows var(--card-raised) + var(--text); nav container padding 12px 16px for the consistent 16px horizontal gutter; logo wordmark in var(--font-display) 700 (desktop, topbar, drawer); avatar borderRadius 50% keeping the 2px gold-border ring; new "Auto-apply" nav item between Interview Prep and Profile with a gold dot + NEW badge (Outfit 600 10px uppercase)
  • •app/(dashboard)/apply/page.tsx (new): /apply index page - the apply/[jobId] flow was orphaned (nothing linked to it), so the nav item needed a destination; lists the user's parsed jobs (id/title/company/location/created_at, newest first, 50 cap) as dash-rows linking to /apply/[jobId], EmptyState pointing to /tailor when no jobs
  • •app/globals.css: .dashboard-main-inner padding 32px -> 32px 40px; .status-badge restyled (var(--radius-sm), Outfit 600 11px uppercase 0.06em); status color vars remapped - applied gold-dim/gold, interviewing rgba(34,197,94,0.1)/score-green, rejected+ghosted rgba(239,68,68,0.1)/score-red, offer left green as before (spec didn't cover it); new .dash-row class (14px 16px padding, border-subtle bottom border, hover to card-raised, 0.15s) and .input-gold-focus class (gold-border + 3px gold-dim ring on focus; !important to beat inline border)
  • •app/dashboard/page.tsx: greeting title Outfit 700 24px -0.02em with 14px Manrope subtitle; stat cards on var(--card-raised) + var(--shadow-md), values Outfit 800 32px -0.03em, labels Manrope 12px uppercase 0.06em; section titles now eyebrow style (Outfit 600 12px uppercase 0.08em muted); all three list sections converted from bordered pills to .dash-row rows
  • •page titles normalized to Outfit 700 24px -0.02em + Manrope 14px muted subtitles: tracker, jobs, interview, networking, billing, strategy (both variants), tailor, apply/[jobId], SettingsClient, ProfileForm, DocumentsClient
  • •per-page padding overrides removed (tracker, tailor, strategy had their own 40px 16px 80px stacking on the layout padding) - the layout's 32px 40px now governs spacing on every inner page
  • •app/(dashboard)/tailor/page.tsx: inputs on var(--card-sunken) with var(--radius), 12px 16px padding, Manrope 14 + input-gold-focus focus ring (URL input and paste textarea); primaryBtn (Parse job / Continue / Tailor / cover letter) switched to the gold treatment (gold-dim bg, gold-border border, gold text, Outfit 600, 12px 24px); new pre-parse placeholder under the input - "Paste a job URL above to get started" with three gold-check hint lines (tailored resume, ATS score, cover letter)
Jul 7, 2026
Improvement

Landing page depth pass - feature hierarchy, section headlines, quote cards, gold glow

  • •app/(marketing)/page.tsx: feature section split into a real hierarchy - three lead cards (Resume tailoring, ATS scoring, Auto-apply engine) at 28px padding with 18px Outfit names and fuller descriptions, plus a compact 5-up supporting row (28px icon tiles, 14px names); eyebrow moved to "YOUR UNFAIR ADVANTAGE" with the old eyebrow text promoted to a proper h2
  • •every section now has an Outfit 700 clamp(24-30px) headline under its eyebrow: "Applying more isn't working." / "Everything you need to get hired" / "Upload once. Tailor every time." / "Free to start. Unlimited for $8."
  • •pain points recast as first-person quotes (figure/blockquote with em-dash attributions like "every senior in recruiting season") in .lp-pain-card - gold-border left rule that intensifies to full gold on hover
  • •gold glow depth pass: primary CTAs (hero, both pricing cards, bottom) get .lp-glow-cta ambient gold glow that strengthens on hover; how-it-works cards get an inset gold glow bleeding down from their gold top border; Pro pricing card now carries the most dramatic treatment on the page (shadow-glow + 48px gold halo + shadow-lg); bottom CTA section wrapped in full-bleed border-subtle hairlines top and bottom
  • •app/globals.css: new .lp-glow-cta, .lp-pain-card, .lp-grid-5 (5/3/1 responsive) classes
Jul 7, 2026
Improvement

Closing CTA section as a hero bookend

  • •app/(marketing)/page.tsx: bottom CTA rebuilt to mirror the hero - Outfit 800 at clamp(36px, 6vw, 64px) with -0.04em tracking, headline now two beats with the closer "Start with an unfair advantage." in gold (echoing the hero's gold line and the brand tagline); section padding 96px inside the hairlines; CTA enlarged (15px, 14px 28px padding) with hero-style microcopy beneath ("Free during early access · No credit card required")
  • •app/globals.css: new .lp-glow-cta-lg for the closing CTA - gold-border ring + 32px halo at rest, full-gold ring + 48px halo and a 1px lift on hover, transform disabled under prefers-reduced-motion
Jul 7, 2026
Improvement

Closing CTA rebuilt as a glowing band with a before/after diff artifact

  • •app/(marketing)/page.tsx: the centered bookend closer (64px headline + gold second line + hairlines) is replaced by a full-width glowing band - var(--card-raised) with a gold border and the page's strongest halo (shadow-glow + 64px gold bloom + shadow-lg). Left column: "YOUR NEXT APPLICATION" eyebrow, the approved "Stop sending the same resume to every job." headline at Outfit 700 clamp(26-38px), the glowing CTA and early-access microcopy. Right column: a product-diff artifact on var(--card-sunken) - the hero's own line "Applied to 100 jobs. Heard back from 1." struck through under a BEFORE label, rewritten as "Applied to 12 jobs. Landed 4 interviews." with a gold left bar under AFTER VANTAGE, plus ATS 84/100 (gold) and INTERVIEWING (green) chips echoing the app's badge language; artifact is aria-hidden illustration
  • •app/globals.css: new .lp-cta-band-grid (1.1fr/0.9fr, single column under 768px); .lp-glow-cta-lg retained for the CTA ring/halo hover
Jul 7, 2026
Improvement

Consistent skeleton loading states across all dashboard pages

  • •app/globals.css: @keyframes shimmer (opacity 0.5 -> 1 -> 0.5, used at 1.4s ease-in-out infinite) and @keyframes fade-in with a .fade-in class (0.2s ease) - the shimmer keyframe previously only existed inside SkeletonLoader's scoped style tag, so tracker's and StrategyReport's skeleton animations silently never ran unless that component happened to be mounted
  • •components/ui/SkeletonLoader.tsx: rewritten to the new spec - var(--card-raised) background (was a linear-gradient sweep, which also violated the no-gradients rule), var(--radius), opacity shimmer, aria-hidden, scoped style tag removed
  • •full-page spinners replaced with layout-matched skeletons: interview (title/subtitle bars + start-session card + 3 session rows), apply/[jobId] (back-link + title bars + 3 section cards)
  • •text/spinner placeholders replaced with skeleton rows: ats ("Loading scores..." -> 4 table-row blocks), networking outreach tracker (centered spinner -> 3 row blocks)
  • •tracker: loading state upgraded from 3 generic bars to the real layout (4 stat-tile skeletons in the stats grid + 5 row skeletons in the table card), now via SkeletonLoader
  • •loaded content fades in via .fade-in on: tracker stats grid + table, ats table, networking tracker table, interview list view, apply/[jobId] page root, StrategyReport report body
  • •no changes needed: jobs (localStorage-cached list, loading is a non-blocking refresh banner), tailor (no page-level gate; autoLoading is an inline banner), documents/settings/profile/billing/strategy wrapper/apply index (server-rendered, data resolves before paint)
Jul 7, 2026
Improvement

Dashboard consistency pass - custom selects, gold primary buttons, unified badges/cards/labels

  • •app/globals.css: new .dash-select class (card-sunken background, var(--radius), appearance:none with an inline SVG chevron background image, gold focus ring, card-raised option styling) and .btn-gold-hover (gold ring + 16px glow on hover, disabled-safe); .status-badge padding tightened to 3px 8px; --status-offer-* remapped to rgba(34,197,94,0.15)/score-green
  • •every native select converted to .dash-select: tracker log form, networking message type + job, interview job picker, ats job + resume version, jobs job-type + date-posted filters, ProfileForm graduation year + years of experience
  • •every primary action button now wears the gold treatment with hover glow: tracker (+ Log application, Log), networking (Find contacts, Generate), interview (Start new session x2), ats (Run ATS check), jobs (Fetch New, Fetch Jobs), tailor (Parse job, Continue, Tailor My Resume, Generate Cover Letter, Log)
  • •status colors standardized in the three remaining indigo/amber maps (tracker, ApplicationDetailModal, apply/[jobId]): applied gold-dim/gold, interviewing green 0.1, offer green 0.15, rejected+ghosted red 0.1; badge shape unified (radius-sm, Outfit 600 11px uppercase 0.06em, 3px 8px)
  • •var(--shadow-md) + var(--border) added to every card missing it: tracker, networking, interview (start + past sessions), ats (history/score-now/insights), tailor, billing, strategy locked card, StrategyReport, ProfileForm, SettingsClient, DocumentsClient, apply/[jobId], dashboard section cards; modals (ApplicationDetailModal, Settings delete confirm) get shadow-lg; stray 12/14px radii normalized to var(--radius)
  • •section labels unified to the eyebrow style (Outfit 600 11px uppercase 0.08em muted): networking numbered sections, interview card titles, ats card titles, StrategyReport (Key Insights / Next steps / Skills) with the green FOCUS HERE and red STOP APPLYING HERE coloring removed
  • •row hover states (card-raised, 0.15s): tracker table rows, ats history rows, networking outreach rows, interview past-session rows, DocumentsClient cards (onMouseEnter/Leave)
  • •interview empty state rebuilt: gold-dim icon tile, "No sessions yet" Outfit 600 15px, "Start a session above to practice with AI feedback" Manrope 13px
  • •jobs initial load: full-page spinner replaced with 4 job-card skeletons (missed in the earlier skeleton audit - the gate is initialLoadDone, not a loading useState(true)); saved-job indigo border swapped for var(--gold-border)
Jul 7, 2026
Fix

Job pickers everywhere now source exclusively from the application tracker

  • •app/(dashboard)/apply/page.tsx: the Auto-apply list now queries applications (company, role, job_id, non-deleted, job_id required) instead of the raw jobs table - applications always carry company/role so "Untitled job" rows can no longer appear; deduped by job_id, rows read "Company · Role / Tracked date"; empty state now points to the tracker ("Auto-apply works from your application tracker")
  • •app/(dashboard)/ats/page.tsx + networking/page.tsx: the jobs dropdown queries already required a tracker application via applications!inner(id) but still surfaced jobs whose applications were soft-deleted; both now filter applications.deleted_at is null
  • •interview job picker already sourced from applications with deleted_at excluded - no change needed; tailor and apply/[jobId] fetch single jobs by explicit id, not lists
Jul 7, 2026
Fix

Full audit - everything that consumes jobs sources from the application tracker

  • •lib/userContext.ts: fetchApplicationHistory filtered applications with .eq('deleted', false) - a column that does not exist (schema is deleted_at) - so all four history queries errored silently and buildUserContext reported an empty application history to every AI route (tailoring, cover letters, strategy feedback, interview prep, outreach); all four now use .is('deleted_at', null), restoring real tracker data as the context source
  • •lib/rateLimit.ts: same phantom-column bug in both application-cap counts (checkLimit + getRemainingLimits) meant the 150-application free-tier cap silently never enforced via the intended path; both now use .is('deleted_at', null)
  • •audit of every from('jobs') call confirmed the remaining surface is correct: job pickers/lists (ats, networking, interview, apply index) are tracker-scoped with soft-deleted applications excluded; by-id job fetches (tailor flow, ats-score, tailor-resume, generate-cover-letter, generate-outreach, interview-prep, answer-question, fill-snippet, extension kit, apply/[jobId]) stay unrestricted deliberately - the parse -> tailor -> log flow operates on a job before it is tracked; .in('id', jobIds) enrichment calls (applications API, interview page) already derive their ids from tracker rows; api/strategy-feedback was already fully tracker-sourced and never touches the jobs table
Jul 7, 2026
Improvement

CustomSelect component replaces every native dashboard dropdown

  • •components/CustomSelect.tsx (new): fully custom React dropdown - card-sunken trigger with rotating SVG chevron, absolute-positioned list (zIndex 50, card-raised, gold-border, shadow-lg, 4px offset, width matches trigger, maxHeight 240 with scroll), options with gold-dim/gold hover and gold 600 selected state, outside-click close via document mousedown listener, position:relative wrapper so the list anchors inside its card, placeholder + disabled support, listbox/option ARIA roles
  • •all 10 native selects replaced: tracker log-form status, networking message type + job, interview job picker (the one that overflowed its card - now flex:1 wrapper, clipped list with scroll), ats job + resume version, jobs job-type + date-posted filters, ProfileForm graduation year + years of experience (numeric values bridged via String/parseInt)
  • •app/globals.css: .dash-select class removed - superseded by CustomSelect; remaining native selects are marketing-only (ContactForm, DocsLayout)
  • •page padding audit re-verified: .dashboard-main-inner (32px 40px) is the single content wrapper for every dashboard page including /apply, /profile, /settings; no page carries its own outer padding
Jul 7, 2026
Improvement

Dashboard polish pass - input focus glow, zebra rows, rich empty states, danger zone, interview mid-section

  • •app/globals.css: dashboard-wide gold focus glow - .dashboard-main input:focus / textarea:focus get the gold-border + 3px gold-dim ring, covering every current and future field with one rule; .dash-row gains nth-child(odd) card-sunken zebra striping (hover still wins); new .dash-card-row class - raised card rows (shadow-md) that hover to gold-border + shadow-glow
  • •app/(dashboard)/apply/page.tsx: rows rebuilt as .dash-card-row cards with a gold auto-apply bolt icon tile, Outfit titles, and a gold "Start " affordance, 10px gaps between cards
  • •app/(dashboard)/interview/page.tsx: new "how practice works" strip between the session starter and past sessions - three raised tiles (Real questions / Answer out loud / Scored feedback) with gold icon tiles; past-session rows get zebra striping
  • •app/(dashboard)/strategy/StrategyReport.tsx: Key Insights cards get the landing how-it-works treatment - 2px gold-border top edge with an inset gold glow, #N markers now gold Outfit
  • •components/SettingsClient.tsx: danger zone is now a distinct red-tinted container (rgba red 0.04 bg, 0.25 red border, var(--radius)) with a red eyebrow label and an Outfit 600 red delete button
  • •empty states upgraded to icon + title + subtitle: ats history, networking outreach tracker, tracker (adds gold icon + gold-hover CTA), jobs feed, and the dashboard home's three section empties via a new SectionEmpty helper (emptyStateStyle removed)
  • •zebra row striping with hover-restore added to tracker table, ats history, networking outreach, and interview session rows
Jul 7, 2026
Improvement

Full SEO pass - metadata, sitemap, robots, structured data

  • •app/layout.tsx: metadataBase from NEXT_PUBLIC_APP_URL, richer default title/description naming the core features and audience, authors/creator Ishvir Chopra, default Open Graph + Twitter card tags
  • •app/(marketing)/layout.tsx: title template "%s — Vantage" scoped to marketing pages only (dashboard titles untouched)
  • •app/(marketing)/page.tsx: thorough landing metadata - absolute title "Vantage — AI Resume Tailoring, ATS Scoring & Auto-Apply for Students", long description naming Ishvir Chopra, CS students/new grads/Waterloo, 15 keywords covering the target queries, canonical /, OG + Twitter tags; JSON-LD @graph with Organization (founder Person Ishvir Chopra) + SoftwareApplication (name, description, url, free offer) injected via script tag
  • •every marketing page normalized to the template pattern with canonical URLs: about (rewritten description naming the founder and audience), contact, privacy, terms, docs index + all 11 docs pages (also fixes "Cover Letters -Vantage Docs" style typos); blog index and changelog had no metadata at all - both added
  • •app/(marketing)/blog/[slug]/page.tsx: generateMetadata now emits the bare post title (template adds the suffix), excerpt description, canonical /blog/[slug], OG article tags with publishedTime + author, Twitter card
  • •app/sitemap.ts (new): all 19 marketing/docs routes with priorities + every blog post from content/blog with its publish date as lastModified
  • •app/robots.ts (new): allows all public pages, disallows the authed app routes (/dashboard, /tailor, /tracker, etc.) and /api, points to sitemap.xml
  • •note: canonical/OG absolute URLs resolve against NEXT_PUBLIC_APP_URL - must be set in Vercel for production
Jul 6, 2026
Update

Chrome extension - version marker for stale-build detection

  • •extension/manifest.json: version bumped 1.0.0 -> 1.1.0
  • •extension/content.js: the "workday fill start" log now includes the extension version (via chrome.runtime.getManifest()), so a stale build that was not reloaded at chrome://extensions is immediately visible in the console. The Enter-commit fix for multiselect search (school, field of study, skills) was already shipped in the previous release; the type -> Enter -> wait -> click sequence is verified by the harness (standard 21/21, custom 17/17, restricted 3/3)
Jul 6, 2026
Fix

Chrome extension - prompt option clicks now register, closest-match selection, websites slot discovery

  • •extension/content.js: suggestion clicks were dispatched as bare element.click(), which Workday's prompt options ignore (same as the Add buttons) - new selectPromptOption clicks with the full pointer sequence, retrying on the option's deepest descendant and any inner checkbox, verifying registration between attempts (school/field-of-study: multiselect container gains the option text or a selectedItem; skills: the tag appears in the section), logging the exact element clicked (outerHTML, 300 chars), and skipping the click entirely when the value is already selected (prevents re-run duplicates)
  • •extension/content.js: new bestOptionMatch replaces first-includes-wins matching in both fillWorkdayPrompt and skills - tiers: exact, exact after stripping a parenthetical qualifier ("Java (Programming Language)" for "Java"), shortest option starting with the term at a word boundary, shortest word-boundary contains, then option-contained-in-term ("MongoDB" for "MongoDB Atlas"); "Python" no longer picks "Python IDLE", "React" no longer picks "React VR"
  • •extension/content.js: websites slot discovery broadened - inputs are found via automation-id, name="url", id ending in "--url", id containing "website", or the visible "URL"/"Website" label (this tenant renders slots with no automation-ids at all); when 0 slots are found the section's field inventory and outerHTML (8k) are dumped so the real markup is visible
  • •harness (scratchpad): prompt options now ignore bare .click() (mousedown-armed, like the real tenant); skills taxonomy includes decoys (Python IDLE, Java Servlets, React VR) with assertions that closest matches are picked and decoys rejected; website slots render as label + name="url" with no automation-ids. Standard 22/22, custom tenant 17/17, restricted skills 3/3
Jul 6, 2026
Fix

Chrome extension - full-name skill queries, single-click prompt selection, degree listbox support

  • •extension/content.js: skills now type the FULL skill name before committing with Enter (the 3-character truncation predated the Enter fix and produced queries like "Pyt"/"Jav"); the input is cleared before each search, and both skills and school searches snapshot the option elements already in the DOM and only match FRESH ones - stale popups from other sections (e.g. school suggestions appearing during a skills search) can no longer be matched or picked
  • •extension/content.js: bestOptionMatch gains an opt-in first-result fallback (used for skills only - the search already ran on the full term) and filters "Select One" placeholder rows; tier order remains exact > exact-minus-parenthetical > word-boundary starts-with > word-boundary contains > option-contained-in-term
  • •extension/content.js: selectPromptOption no longer double-clicks - when the option contains a radio/checkbox that control is the single primary target (clicking the div then the radio toggled the selection back off, which is why school appeared unselected); success signals now include the control becoming checked and the popup closing, and the one alternate click is skipped whenever the control already holds the selection
  • •extension/content.js: new fillWorkdayListbox for standard listbox dropdowns (button[aria-haspopup="listbox"] -> [role="listbox"] options) wired into education for the Degree field, filled from kit education[].degree and left blank without kit data; keeps any existing selection
  • •app/api/extension/kit/route.ts: kit education entries now include degree, read from the optional profiles.degree column (same graceful-degradation pattern as location; optional migration: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS degree text)
  • •harness (scratchpad): school options are now radio-based with toggle semantics and a popup that stays open (asserts exactly one click and no toggle-off, plus skills immunity to the stale school popup); skills search is token-based and records committed queries (asserts full names, not "pyt"); degree listbox mock with armed clicks. Standard 23/23, custom tenant 19/19, restricted skills 3/3
Jul 6, 2026
Feature

Tailoring results as a per-bullet diff view; PDF flow removed

  • •app/api/tailor-resume/route.ts: rewritten to return structured JSON instead of raw resume text/HTML. The AI now returns { changes: [{ section, original, tailored, reason }], tailored_resume_text, skill_gaps } in one generateJSON call (60s timeout, 6000 token cap); changes are sanitized server-side (well-formed fields only, original must differ from tailored, capped at 60). The HTML surgical-edit mode, profiles.resume_html fetch, and the entire PDF generation/upload block (generateResumeBuffer, storage upload, pdf_url) are removed - response is now { document, changes, skillGaps, atsScore }. Tailoring rules preserved: no invented facts, metrics verbatim, bullets under 20 words, max 1-2 keywords per bullet, unchanged bullets stay out of changes. Inline ATS scoring and event tracking unchanged; console.log calls removed
  • •app/(dashboard)/tailor/page.tsx: Tailored Resume tab is now a stacked diff grouped by resume section (uppercase section labels): original bullet in muted strikethrough, tailored bullet highlighted green, one-line reason underneath, a "Copy bullet" button per change, and a "Copy all tailored bullets" button at the top that copies the full updated resume as plain text. PDF download buttons removed from the resume tab (cover letter print-to-PDF untouched); after tailoring the page lands on the diff tab; falls back to the full tailored text when no per-bullet changes are returned (e.g. older documents)
  • •lib/ai.ts: generateJSON accepts an optional maxTokens parameter (default 3000, unchanged for existing callers) so the tailor route can request 6000 tokens for the full resume + changes payload
  • •lib/generatePdf.ts: now unused, kept in place in case the PDF flow returns
Jul 6, 2026
Improvement

Tailoring - active JD keyword weaving, per-entry grouping in the diff view, tighter cards

  • •app/api/tailor-resume/route.ts: prompt rewritten around a primary goal of mirroring the job description's language - the AI scans required_skills, key_responsibilities, and keywords and rewrites bullets that touch those areas using the job's exact terminology (e.g. React/Node.js/PostgreSQL/Jest/Agile named where the work plausibly involved them), still capped at 2 keywords per bullet and never forced into unrelated bullets; the reason field must NAME the keyword when one was woven in ("Added PostgreSQL keyword from job description"). Each change now includes an entry field - the company or project name the bullet belongs to, as written in the resume - sanitized alongside the other fields
  • •app/(dashboard)/tailor/page.tsx: diff view now groups two levels deep - section heading, then the company/project name as a subheading above its bullets (changes without an entry render directly under the section); bullet cards' horizontal padding halved (14px 16px -> 14px 8px), vertical unchanged
Jul 6, 2026
Fix

Reverted shadcn init - restored original design system

  • •app/globals.css: restored from git HEAD - removed shadcn's oklch variable overrides (--card, --muted, --border, --accent, --radius were clobbered), the tw-animate-css and shadcn/tailwind.css imports, the sidebar/chart/popover variable sets, the .dark class block, and the @layer base overrides; all original CSS variables and app styles are back
  • •app/layout.tsx: restored from git HEAD - removed the Geist font import and cn() className on the html element
  • •package.json / package-lock.json: restored from git HEAD - removed @base-ui/react, class-variance-authority, clsx, lucide-react, tailwind-merge, tw-animate-css, and the shadcn dev dependency; node_modules pruned via npm install
  • •deleted shadcn-created files: components.json, components/ui/button.tsx, lib/utils.ts (cn helper), .mcp.json (shadcn MCP server registration), skills-lock.json and .agents/ (shadcn skill files), plus the enabledMcpjsonServers shadcn entry in .claude/settings.local.json
  • •tailwind.config.ts was never modified by shadcn (Tailwind v4 config lives in CSS) - no changes needed
Jul 4, 2026
Fix

Chrome extension - Workday multi-entry auto-add fixed; fill logic now verified by a DOM harness

  • •extension/content.js: ensureWorkdayEntries held the section element across Add clicks - Workday replaces the whole section node when a panel is added, so the entry count was polled on the detached old subtree and every Add click after the first hit a detached button (its event never reaches the live DOM). It now takes a section getter and re-resolves the section and Add button fresh on every iteration, with per-click logging. This was the remaining cause of experience/websites sections getting at most one auto-added entry
  • •extension/content.js: skills tag verification now also accepts the clicked suggestion's text, not just the kit wording (tenants render "React" for a kit skill "React.js"), preventing false "did not register" reports
  • •verification: built a jsdom harness (scratchpad, not committed) that loads the real content.js into a replica Workday My Experience page with hostile behaviors - blur re-renders replace field nodes, Add replaces the section node, skills only register via suggestion click and replace their search input, unknown skills get no suggestions, currently-work-here removes the end-date field. 19/19 assertions pass: 23 fields filled across experience (2 panels, one auto-added), education (auto-added), skills (2 tags, bogus skill skipped), websites (slots 1-3 = linkedin/portfolio/github), and a second run is a verified no-op (no duplicate panels, pills, or overwrites)
Jul 4, 2026
Fix

Chrome extension - Workday Add-button pointer clicks, restricted skills early abort, LinkedIn/Websites dedupe

  • •extension/content.js: new simulateClick helper - scrolls the element into view then dispatches the full pointerdown/mousedown/pointerup/mouseup/click sequence (Workday's React Add buttons ignore bare element.click()); ensureWorkdayEntries uses it, logs the Add button's outerHTML before clicking, waits up to 4s per click (500ms polls), and falls back to plain element.click() before giving up; findAddButton also scans shadow roots for Add buttons
  • •extension/content.js: skills - Workday's literal "No Items." suggestion row is now detected as a definitive miss (stops polling immediately instead of waiting out the 3.2s timeout), and after 3 consecutive misses with zero successes the section aborts with "skills field uses a restricted list, manual entry required" instead of attempting all 15 kit skills against a taxonomy that rejects everything
  • •extension/content.js: websites - LinkedIn is excluded from website slots whenever a dedicated LinkedIn field (e.g. socialNetworkAccounts--linkedInAccount) exists on the page, since that field is filled separately; slot order is portfolio github in that case
  • •harness (scratchpad): Add buttons now ignore bare .click() and require the pointer sequence (reproduces the "Add click had no effect" log before the fix); unknown skills render a real "No Items." row; dedicated LinkedIn field uses the real automation-id; added a RESTRICTED_SKILLS scenario asserting zero tags, the abort message, and exactly 3 attempts for 6 kit skills. Main scenario 20/20, restricted scenario 3/3
Jul 4, 2026
Fix

Chrome extension - Workday label-based field discovery for tenants without automation-ids, section DOM dumps

  • •extension/content.js: root cause of "job-title inputs = 0 / school inputs = 0" with a section found - two failure modes fixed. (1) The heading-fallback section lookup returned the first ancestor containing an Add button, which is often the header ROW holding the button but none of the panels; it now walks up to 5 levels preferring the smallest ancestor that actually contains form fields (stopping at main/[role=main]), returning the Add-button container only for truly empty sections, where counts self-heal after the first add re-resolves. (2) Tenants that render experience/education panels with no data-automation-id at all are now handled by label-based discovery: inputsByLabelIn resolves fields from visible labels (for="" association label's sibling container bounded ancestor walk); anchors fall back to /job title/ and /school|university/ labels, per-field lookups fall back to /company/, /location/, /description/ labels, currentlyWorkHere falls back to the "currently work here" label, and dates fall back to the labelled From/To containers - filling month+year spinner pairs, two generic inputs, or a single combined MM/YYYY input, whichever the tenant renders. panelsFor groups anchors into role="group" wrappers or minimal unique ancestors; ensureWorkdayEntries now takes a count function
  • •extension/content.js: diagnostics - each section now logs a field inventory (tag + automation-id/id/name/type/placeholder/aria-label for every input) and the first panel's outerHTML (15k cap); when 0 panels are found despite a section, the whole section's outerHTML is dumped so the tenant's real structure is readable straight from the console
  • •harness (scratchpad): new CUSTOM_TENANT=1 scenario - experience/education with zero automation-ids, label-wired fields, Add button inside the header row, experience pre-expanded with 2 panels, education empty; 14/14 assertions pass (label fills, dates, checkbox, auto-add via header Add, grad year in "To (Actual or Expected)", degree untouched). Standard scenario 20/20, restricted skills 3/3
Jul 4, 2026
Fix

Chrome extension - Workday spinner dates via keydown, multiselect school prompts, partial skill queries, websites fill-only

  • •extension/content.js: date spinbuttons (role="spinbutton" / dateSectionMonth|Year automation-ids) build their value from keydown events - text insertion corrupted "2024" into "0004"; new workdaySpinnerType dispatches keydown/keypress/keyup per digit with human-like delays, verifies the digits landed, and setTextValue routes spinner elements to it automatically (all date fills across experience/education inherit the fix)
  • •extension/content.js: education school on multiselect tenants (multiSelectContainer search input, placeholder="Search") is now filled as a prompt - new fillWorkdayPrompt types the school name, waits up to ~4s for suggestions, clicks the exact/contains match, and treats the "No Items." row as a definitive miss; isWorkdayPrompt routes between plain-input and prompt filling; education panels iterate in reverse and fill graduation year before school, since selecting a school consumes its search input and would shift anchor indices. Degree (button-listbox) and field of study stay blank - the kit has no degree/field-of-study data
  • •extension/content.js: skills - types only the first 3 characters as the query (some tenant taxonomies need partial matches) with a 1000ms first wait, still matching suggestions against the full skill name; restricted-list abort message updated
  • •extension/content.js: websites - no programmatic Add (this tenant's Websites Add button ignores synthetic clicks); fills however many slots the user pre-added, logs "no URL slots found - click Add manually, then re-run" when there are none, and "click Add manually and re-run for the rest" when URLs outnumber slots; LinkedIn still excluded when its dedicated field exists
  • •harness (scratchpad): spinbutton mocks corrupt execCommand insertion into the reported "0004" and only build values from keydown digits (date assertions now prove the keydown path); custom-tenant education uses a multiselect school prompt with suggestion popup + pill replacement and a Select One degree button; websites pre-expanded with 2 slots and asserted no auto-add. Standard 20/20, custom tenant 14/14, restricted skills 3/3
Jul 4, 2026
Update

Codebase-wide - replaced em/en/long dashes with regular hyphens

  • •57 files: replaced every em dash, en dash, figure dash, horizontal bar, minus sign (U+2212), and box-drawing dash (U+2500) with a regular "-" across all source, content, and config files (431 occurrences). Comments, log strings, UI copy, MDX content, and SQL affected; no functional changes. Extension harness re-verified: standard 20/20, custom tenant 14/14, restricted skills 3/3
Jul 4, 2026
Fix

Interview prep - tracker job dropdown was always empty

  • •app/(dashboard)/interview/page.tsx: the dropdown was built from an embedded applications->jobs join whose error was silently ignored, and it filtered out every application without a joined job record - which excludes all manually-added tracker entries (job_id is null for those). Now loads applications without the embed (matching the /api/applications pattern), enriches titles/companies from the jobs table in a second query, includes manual tracker entries (they start sessions via the title/company payload the generate API already accepts), dedupes by job_id or company+role, surfaces the load error in the UI instead of swallowing it, and shows "No applications in your tracker yet" when the tracker is genuinely empty. Past-session rows no longer mis-match sessions with null job_id
Jul 4, 2026
Fix

Chrome extension - Workday spinner overwrite, multiselect activation, websites manual-add notice

  • •extension/content.js: date spinbuttons are now exempt from the keep-existing-value rule - a previous fill attempt can leave corrupt values ("0", "2000") that were being skipped with "keeping existing value"; workdaySpinnerType now selects all existing content first, types each digit as keydown/keypress/keyup with a fixed 50ms gap, and fires a change event after the digits (logs "overwriting spinner value" when replacing)
  • •extension/content.js: new activatePromptInput - Workday multiselects only open their suggestion dropdown after the multiSelectContainer itself is clicked, so both fillWorkdayPrompt (education school / field of study pattern) and the skills loop now click the container (pointer sequence), wait 200ms, focus the inner input, wait 200ms, type the query character by character with 50ms delays, then wait 800ms for server-side suggestions; option matching now also accepts data-automation-id="multiSelectOption" via a shared WD_OPTION_SEL selector
  • •extension/content.js + extension/popup.js: websites - directFill now returns websitesNeedManualAdd (set when the section has no URL slots or URLs outnumber slots, since this tenant's Add button ignores programmatic clicks); the popup success message reads "Fill complete: N fields..." and appends "Note: manually click "Add Another" in the Websites section and re-run to fill your portfolio and GitHub URLs." only when the flag is set
  • •harness (scratchpad): multiselects now require a container click (not an input click) before rendering suggestions - reproducing the "No Items." failure for the old interaction; custom scenario pre-corrupts panel 0's date spinners with 0/2000 and asserts they are overwritten to 01/2025, and has a single website slot asserting the manual-add flag; standard asserts the flag stays false when slots suffice. Standard 21/21, custom tenant 17/17, restricted skills 3/3
Jul 4, 2026
Fix

Chrome extension - Workday dates via ARIA arrow-stepping, prompt search committed with Enter

  • •extension/content.js: date spinbuttons rewritten around the real widget (from the user's panel outerHTML dump: aria-valuenow/valuemin/valuemax, help text "use right and left arrows"). workdaySpinnerType now tries three verified strategies in order: (A) native value setter + beforeinput/input events, (B) ArrowUp/ArrowDown stepping - the ARIA spinbutton pattern guarantees arrow handling, reading aria-valuenow after every press for exact feedback, seeding empty spinners with one press and bailing if a press has no effect or the distance exceeds 60 steps, (C) per-digit keydowns; each strategy is verified against aria-valuenow/value before the next runs, and the log names which strategy landed
  • •extension/content.js: multiselect prompts (school, field of study, skills) - Workday's selectinput widgets only run their backend search when the query is committed with Enter; every query without it shows "No Items.". fillWorkdayPrompt and the skills loop now dispatch Enter keydown/keyup after typing, then poll for suggestions
  • •extension/content.js: selector hardening from the real tenant dump - experience anchors also match input[name="jobTitle"] / id$="--jobTitle", company matches input[name="companyName"], location input[name="location"], education anchors match id$="--school"
  • •websites: unchanged - already fills only pre-existing slots and sets the popup "click Add Another manually" note when slots are missing (verified: flag true with leftover URLs, false when slots suffice)
  • •harness (scratchpad): spinner mocks now replicate the real tenant exactly - typed digits ignored, direct value writes snap back to widget state (React controlled input), only arrows change aria-valuenow (seeding month=1/year=2026 on first press); prompt popups render only on Enter. First-run logs confirm every date fills via arrow-stepping. Standard 21/21, custom tenant 17/17, restricted skills 3/3
Jul 2, 2026
Fix

Chrome extension - kit unwrap bug caused 0-field fills (Workday and all platforms)

  • •extension/popup.js: fixed the root cause of "No fillable fields matched your profile" - /api/extension/kit returns { kit } at the top level (ok() does not wrap responses in data), but the popup read kitData.data.kit, so the content script always received an empty kit and every Tier 1 value was blank; now reads kitData.kit with a data.kit fallback. Added an explicit error message when the profile kit is genuinely empty, and surfaced content-script fill errors instead of reporting them as "no fields matched"
  • •extension/content.js: findWorkdayInputByLabel fixed - Workday labels carry data-automation-id="formLabel" so closest('[data-automation-id]') matched the label itself (which never contains the input); now resolves the label's for="" association first, then walks up to the formField container. fillWorkdayDropdown now polls up to 3s for async search results instead of a fixed 600ms wait, matches promptLeafNode options, and properly closes the popup on failure (Escape keydown+keyup + outside click) instead of leaving the "How did you hear about us" dropdown hanging open
Jul 2, 2026
Fix

Chrome extension - Workday stale-element fills, skills tag selection, website slot mapping, full fill diagnostics

  • •extension/content.js: root cause of sections not filling while simple fields worked - the WORKDAY_FIELD_MAP loop re-queries each element right before filling, but the section fillers captured panel/input references up front; Workday re-renders React subtrees on every blur (which our typing dispatches), detaching those references so focus() no-ops and execCommand typed into nothing while still being counted as filled. All section fillers (experience, education, skills, websites) now re-query the section, panel, and field fresh at each individual fill. workdayTypeValue now verifies the element is attached and actually holds focus before typing, and verifies the value stuck afterward - setTextValue no longer counts or marks fields whose value did not land
  • •extension/content.js: skills rewritten as a strict tag loop - type one skill poll up to 3.2s for the suggestion dropdown click the matching suggestion poll up to 2.4s until it registers as a tag next skill; the search input is re-queried per skill (it detaches after each tag is added); skills with no suggestion are cleared and skipped
  • •extension/content.js: websites section fills one URL per slot in fixed order linkedin portfolio github, re-querying slot inputs before each fill, deduping against URLs already on the page, and auto-adding slots via Add Another when fewer slots than URLs
  • •extension/content.js: DEBUG flag enabled with full fill diagnostics - logs kit fields with data, each section element found (or the page's actual data-automation-id values matching the section when lookup misses), panel count vs kit entries, and per field: the exact element targeted, the value tried, and whether it stuck or why it was skipped (not found / no kit value / hidden / existing value / focus stolen / value did not stick); collectPanels now prefers Workday's role="group" entry wrappers
Jul 1, 2026
Update

Chrome extension auto-fill overhauled - two-tier fill, zero hallucination policy

  • •extension/content.js: complete rewrite. Platform detection (Workday / Greenhouse / Lever / generic); Tier 1 direct fill maps name, email, phone, LinkedIn, GitHub, portfolio, location, current company/title straight from the kit with priority-ordered field matching (name/id/automation-id label placeholder aria-label); fields with no kit data are left blank, never guessed. Workday fills via execCommand character typing with 20-50ms delays and blur per field; Greenhouse uses nativeSetter + React Select menus with __remixContext option extraction and demographic dropdowns defaulted to "I don't wish to answer"; Lever fills name/org/urls[LinkedIn|GitHub|Portfolio], country select by name, label-click checkboxes, and only decline options in the #countrySurvey demographic pass. Removed hardcoded per-job Greenhouse question IDs, removed Lever survey fabricated defaults (age 21-29, Bachelor), removed pronoun "Use name only" default (pronouns now only fill if kit.pronouns is set). "How did you hear about us" always resolves to "Job Board" or stays blank. Consent checkboxes checked; demographic checkboxes never touched. Fill count reports only fields actually changed.
  • •extension/popup.js: orchestrates the two-tier flow - DIRECT_FILL first (no AI), then EXTRACT_QUESTIONS (open-ended only), then /api/extension/ai-fill, then FILL_ANSWERS; total = sum of real fills; AI failure no longer discards Tier 1 fills; removed console logging
  • •app/api/extension/ai-fill/route.ts: deterministic server guards resolve "how did you hear" (Job Board or null), demographics (decline option or null), and any Tier 1-shaped label (null) before the AI is called; rewritten system prompt forbids inventing companies/organizations/websites/referral sources and requires null for anything not in the profile; post-AI validation snaps dropdown answers to verbatim option text or drops them, and re-nulls demographic/Tier 1 answers; deterministic answers still returned if the AI call fails
  • •app/api/extension/kit/route.ts: kit now includes skills[], education[] (derived from university + graduation_year), location (from optional profiles.location column, gracefully empty if the migration has not run), and pronouns: null; referralSource changed from "LinkedIn" to "Job Board"; removed the speculative Gemini call that generated generic application answers on every kit fetch; removed console.error debug logging
  • •optional DB column: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location text;
Jun 9, 2026
Update

Cerebras model updated to gpt-oss-120b

  • •lib/ai.ts: updated Cerebras model string from llama3.1-70b to gpt-oss-120b in generateTextCerebras
May 25, 2026
Fix

PDF printBackground, spacing, and tailor tech stack preservation

  • •lib/generatePdf.ts: printBackground changed falsetrue; -webkit-print-color-adjust and print-color-adjust: exact added to body; h2 margin tightened to 5px 0 2px; p margin changed to 0 0 2px; letter-spacing on h2 changed to 0.05em
  • •app/api/tailor-resume/route.ts: rule 15 added - tech stack line under project headings must be preserved exactly, not removed or shortened
May 25, 2026
Update

Profile form - generic placeholders and month picker date inputs

  • •components/ProfileForm.tsx: skills placeholder changed to "e.g. Project Management, Data Analysis, Public Speaking"; target roles placeholder changed to "e.g. Marketing Manager, Financial Analyst, Product Designer"; experience job title placeholder changed from "Software Engineer" to "Account Manager"
  • •components/ProfileForm.tsx: experience start_date and end_date inputs changed from type="text" to type="month"; styled with background #1a1a1a, border 1px solid #2a2a2a, color #f2f2f2, borderRadius 6px, padding 8px 12px, colorScheme dark; end_date remains disabled and cleared when Current role is checked
May 25, 2026
Fix

Networking - inline delete confirmation, remove AI-generated contact fallback

  • •app/api/find-contacts/route.ts: removed generateFallbackContacts AI fallback entirely; removed ai_generated field from Contact interface; search now cascades LinkedIn via Jina Google via Jina only; AI extraction prompt explicitly forbids fabricating contacts; returns empty array when no real contacts found
  • •app/(dashboard)/networking/page.tsx: removed ai_generated field from Contact interface; removed AI-suggested amber banner; removed AI badge from ContactCard; replaced window.confirm delete dialog with inline "Delete? Yes / Cancel" confirmation using confirmingDeleteId state
May 25, 2026
Update

Networking - Apollo People Search replaces Jina/AI contact scraping

  • •app/api/find-contacts/route.ts: replaced Jina + AI extraction pipeline with a direct POST to Apollo api/v1/mixed_people/api_search; sends person_titles array (recruiter, talent acquisition, technical recruiter, hiring manager, engineering manager, hr manager, + optional role param) and q_organization_name; maps response to { id, name (first_name + last_name_obfuscated), title, company, linkedin (search URL), hasEmail, source: 'apollo' }; returns { contacts, total, message }; removed Jina fetch, generateJSON, buildUserContext, and all scraping logic
  • •app/(dashboard)/networking/page.tsx: Contact interface updated to Apollo shape (id, linkedin, hasEmail, source); ContactCard updated - "Search on LinkedIn ↗" link always shown (search URL), "Has email on file" greyed badge with tooltip shown when hasEmail is true; prefillFromContact uses contact.linkedin instead of contact.linkedin_url
  • •.env.local: APOLLO_API_KEY= placeholder added (fill in master key from app.apollo.io/settings/integrations/api)
  • •new env var required: APOLLO_API_KEY (master key - the people search endpoint requires it)
May 22, 2026
Update

parse-profile extracts work experience and projects from resume

  • •app/api/parse-profile/route.ts: prompt extended to extract experience (company, title, start_date, end_date, current, location, bullets) and projects (name, description, url, tech_stack, bullets); ParsedProfile interface updated with WorkExperience and Project types; upsert payload always writes experience and projects arrays; select returns both new columns
May 22, 2026
Feature

Experience section on profile page - editable cards with inline save

  • •components/ProfileForm.tsx: WorkExperience interface exported; experience state initialized from profile; Experience section added below Target Roles with per-entry dark cards (title, company, start/end date, current-role checkbox, location, bullet list with add/remove); experience included in handleSubmit and handleUploadComplete sync
  • •app/(dashboard)/actions.ts: Profile interface gains experience field; updateProfile upsert includes experience
  • •app/(dashboard)/profile/page.tsx: select query includes experience column
May 22, 2026
Feature

Projects section on profile page - editable cards with tag-based tech stack

  • •components/ProfileForm.tsx: Project interface exported; projects state + per-card projectTechInputs state; Projects section added below Experience with per-entry dark cards (name, URL, description, tech stack tag input matching skills UX, bullet list with add/remove, × delete); projects included in handleSubmit and handleUploadComplete sync
  • •app/(dashboard)/actions.ts: Profile interface gains projects field; updateProfile upsert includes projects
  • •app/(dashboard)/profile/page.tsx: select query includes projects column
May 22, 2026
Update

Experience and projects injected into all AI prompts

  • •lib/userContext.ts: WorkExperienceEntry and ProjectEntry interfaces exported; experience and projects added to UserContext interface; fetchProfile select includes both columns; buildUserContext return includes both; formatContextForPrompt renders WORK EXPERIENCE and PROJECTS sections above resume snippet
  • •app/api/extension/kit/route.ts: Kit interface gains experience/projects; profile select includes both; returned kit includes both arrays
  • •app/api/extension/ai-fill/route.ts: system prompt instructs AI to draw from WORK EXPERIENCE and PROJECTS when answering open-ended questions
  • •app/api/tailor-resume/route.ts: userPrompt instructs AI to use structured work history for tailored bullet points and surface relevant projects when they match required_skills
  • •app/api/interview-prep/generate/route.ts: expSummary and projSummary built from ctx.experience/projects; behavioral_questions prompt grounded in candidate's actual companies and projects using STAR framing
  • •app/api/generate-outreach/route.ts: mostRecentExp and relevantProject selected from ctx; message instructions for all three types reference candidate-specific background details
May 22, 2026
Fix

PDF generation - no browser headers, HTML-preserved formatting, 1-page layout

  • •app/api/parse-resume/route.ts: after text extraction, calls Groq to convert resume to structured HTML (h1 name, h2 sections, ul/li bullets, a href links, strong/em); extractBodyContent() strips any accidental html/head/body wrappers; HTML saved to profiles.resume_html; response includes resumeHtml field
  • •app/api/tailor-resume/route.ts: parallel fetch adds profiles.resume_html; when HTML is available, uses surgical HTML update prompt (only modifies li text and summary, preserves all tags/hrefs/structure, SKILL_GAPS appended after HTML); falls back to text generation when HTML is absent
  • •app/(dashboard)/tailor/page.tsx: downloadAsPDF injects @page { margin: 0 } + body { margin: 0.5in 0.6in } to suppress browser print headers; renders HTML content directly when body is HTML, wraps plain text in pre; isHtmlContent() and stripHtml() helpers added; copyToClipboard strips HTML tags for plain-text paste; resume preview uses dangerouslySetInnerHTML for HTML content, pre for text
  • •new DB column required: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS resume_html text;
May 22, 2026
Feature

Cover letter - proper letter format + personal template support

  • •app/api/generate-cover-letter/route.ts: parallel fetch adds profiles (full_name, phone, email, linkedin_url, portfolio_url, cover_letter_template); builds letterHeader from profile contact info + today's date; two prompt paths - if cover_letter_template exists, adapts template keeping user's voice; else generates full letter with header/date/Dear/3-4 paragraphs/Sincerely block; key_responsibilities added to job select; output token limit raised to 1500
  • •components/ProfileForm.tsx: cover_letter_template added to Profile interface, state, useEffect sync, handleSubmit payload; Cover Letter Template section added before Save button - labeled textarea with placeholder hints, {company}/{role}/{hiring_manager} placeholder guide in helper text, Clear template button
  • •app/(dashboard)/actions.ts: Profile interface and upsert include cover_letter_template
  • •app/(dashboard)/profile/page.tsx: select query includes cover_letter_template
  • •new DB column required: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS cover_letter_template text;
May 22, 2026
Feature

Puppeteer PDF generation - pixel-perfect server-side resume PDF via Chromium

  • •lib/generatePdf.ts created: generateResumeBuffer(htmlBody) uses @sparticuz/chromium-min + puppeteer-core to render HTML to a Letter-format PDF; CHROMIUM_EXECUTABLE_PATH env var selects local Chrome path (dev) or CDN tarball URL (prod); RESUME_CSS template uses Times New Roman 10.5pt, h1/h2/h3 hierarchy, ul/li with correct indentation, real a href links; returns null gracefully when env var is unset
  • •app/api/tailor-resume/route.ts: export const maxDuration = 120; after saving document, calls generateResumeBuffer when content is HTML; uploads PDF buffer to Supabase Storage bucket pdfs at tailored-resumes/{userId}/{docId}.pdf; stores public URL in documents.pdf_url via update; returns pdfUrl in response
  • •app/api/parse-resume/route.ts: for non-DOCX uploads, stores fileUrl in profiles.resume_pdf_path alongside resume_html update
  • •app/(dashboard)/tailor/page.tsx: Doc interface gains pdf_url field; tailorResume() captures pdfUrl from API response; Download PDF button renders as anchor opening pdf_url in new tab when available, falls back to existing print path otherwise
  • •new DB columns required: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS resume_pdf_path text; ALTER TABLE documents ADD COLUMN IF NOT EXISTS pdf_url text;
  • •new npm packages: puppeteer-core @sparticuz/chromium-min (already installed)
  • •new env var required: CHROMIUM_EXECUTABLE_PATH (local Chrome path for dev; CDN tarball URL for Vercel prod)
May 22, 2026
Fix

Surgical tailor prompt + professional PDF CSS template

  • •app/api/tailor-resume/route.ts: HTML path system prompt rewritten with 12 explicit absolute rules - only li text may change; section headings, name, contact info, job titles, company names, dates, href attributes all frozen; bullet count per section locked; 20-word bullet cap; filler phrases (demonstrating my ability to, showcasing my, leveraging my) banned; direct tone preserved
  • •lib/generatePdf.ts: RESUME_CSS replaced - @page margin 0.55in 0.65in; body Arial/Helvetica 10.5px; h1/h2 Georgia serif (18px centered bold name, 11px uppercase section headings with 1px solid border); h1+p contact line 10px centered; ul padding-left 12px; li 10.5px line-height 1.4 margin 2px; a links black underline
May 22, 2026
Fix

Tailor metrics preservation + PDF bullet rendering + hyperlink retry

  • •app/api/tailor-resume/route.ts: two new absolute rules added to HTML path prompt - rule 11 preserves all specific numbers, percentages, dollar amounts, and metrics verbatim; rule 12 requires every rewritten bullet to retain every quantitative fact from the original
  • •lib/generatePdf.ts: CSS updated - list-style-type: disc on ul and li so bullets render as disc not plain text; ul margin: 0 padding-left: 18px; li margin: 2px 0 line-height: 1.4; p margin: 2px 0 font-size: 10.5px; h1 font-size 18px16px; h2 margin tightened to 8px 0 3px; body line-height 1.41.3
  • •app/api/parse-resume/route.ts: after extractBodyContent, counts <a tags in output; if zero links found and profile has linkedin_url or github_url, reruns AI with an explicit prompt that hard-codes the exact href values in the contact line instruction; logs retry link count
May 22, 2026
Fix

PDF spacing, bullet disc, metrics preservation, keyword stuffing, project link injection

  • •lib/generatePdf.ts: h1 15px margin 0 0 2px; h2 10.5px bold uppercase border-bottom margin 6px 0 2px; p margin 1px 0; ul margin 0 padding-left 16px; li list-style-type disc margin 1px 0 line-height 1.35; body line-height 1.3 - all tightened to fit one page
  • •app/api/tailor-resume/route.ts: rule 11 preserves all metrics verbatim including ~20% and R² notation; rule 12 requires every quantitative fact to survive the rewrite; rule 13 bans forcing keywords into unrelated bullets; rule 14 caps keywords at 1-2 per bullet
  • •app/api/parse-resume/route.ts: ProfileLinks gains projects field; profiles select includes projects; builds projectLinksBlock from projects where url is non-null; prompt injects PROJECT LINKS section with exact URLs; two new rules instruct AI to match project links by name and never fabricate URLs; retry path also injects project links block
May 18, 2026
Feature

AI-powered form fill via Cerebras

  • •app/api/extension/ai-fill/route.ts created: POST endpoint with token auth; accepts extracted form questions, builds user context via buildUserContext, calls generateJSONCerebras to generate field answers, returns { fields: [{ label, answer }] }
  • •extension/content.js: added getLabel() for human-readable label extraction, fill() helper, extractFormQuestions() to scrape all form fields, fillWithAIAnswers() to match AI answers back to DOM elements (native inputs + custom comboboxes); updated message listener for EXTRACT_QUESTIONS and FILL_ANSWERS
  • •extension/popup.js: rearchitected handleFill to use four-step flow - token validation, EXTRACT_QUESTIONS, POST ai-fill, FILL_ANSWERS; replaced sendFill with generic sendMessage helper
May 18, 2026
Update

.docx resume upload support + graduation year range fix

  • •components/ResumeUpload.tsx: accept .pdf/.docx/.doc; validate by extension and MIME type; set correct content-type on Supabase storage upload; pass fileName to parse-resume; updated button label and error message
  • •app/api/parse-resume/route.ts: branches on file extension / Content-Type header - uses mammoth for DOCX/DOC, pdf-parse for PDF; removed pageCount from response (not used downstream)
  • •components/ProfileForm.tsx: graduation year range now 1970 to currentYear+7, generated dynamically, shown in descending order; default and sync logic updated from hardcoded 2026 to currentYear
  • •new package: mammoth
May 18, 2026
Fix

Extension - phone dropdown filter, proximity panel detection, and open-ended question fill

  • •extension/content.js fillCustomDropdowns: phone/country skip now checks trigger className and aria-label for "phone"/"country"/"flag" in addition to container tel input - catches dropdowns where the phone field is not a sibling
  • •extension/content.js fillCustomDropdowns: adds options.length > 50 guard after panel opens - dispatches Escape and skips when the open panel is a country/state list
  • •extension/content.js fillCustomDropdowns: open panel detection replaced with proximity-based search - finds allPanels, picks the one whose top is within 200px of the trigger's bottom, falls back to last panel; prevents a distant open panel from being used for the wrong trigger
  • •extension/content.js textarea loop: open-ended question fill added - if no cover letter key matches, checks label for "excites you", "career goals", "why do you want", "why are you interested", "tell us about yourself", "what would you contribute", "long-term", "passion", "motivated" and fills from kit.applicationQuestions.excited or .goals
  • •app/api/extension/kit/route.ts: Kit interface gains applicationQuestions field
  • •app/api/extension/kit/route.ts: job query updated to select title, company, key_responsibilities, company_description
  • •app/api/extension/kit/route.ts: after fetching cover letter and saved questions, generates applicationQuestions via generateJSON with 15s timeout - excited, goals, why_company, contribution - non-critical, fails silently
May 17, 2026
Feature

Strategy report - data-driven job search insights

  • •app/api/strategy-feedback/route.ts created: GET endpoint with auth, rate limit (2/month), 15-app minimum gate, 5-app cache invalidation threshold, analytics from applications + ats_scores tables (response rate by role, ATS correlation by outcome, top 5 missing keywords), Groq AI generation, upsert to strategy_feedback table on user_id conflict
  • •app/(dashboard)/strategy/page.tsx created: server component with locked state (progress bar, 15-app target) for insufficient data, unlocked state renders StrategyReport client component
  • •app/(dashboard)/strategy/StrategyReport.tsx created: client component, fetches GET /api/strategy-feedback on mount, renders key insights cards, focus/avoid role badges, ATS correlation callout, numbered next steps, skill gap amber badges with Add to profile link, last-updated footer with Refresh button (?force=true)
  • •components/Sidebar.tsx updated: Strategy nav item changed from comingSoon to locked (shows lock icon when applicationCount < 15, navigable and unlocked when >= 15)
May 17, 2026
Update

Job feed "Tailor resume" pre-fill + secondary Groq key for job scoring

  • •app/(dashboard)/jobs/page.tsx updated: "Tailor resume" Link changed from ?url= to ?prefill= so the tailor page receives the Adzuna redirect URL as a prefill param
  • •app/(dashboard)/tailor/page.tsx updated: useEffect on mount reads jobId or prefill query params; parseJob() accepts optional overrideUrl to bypass state; goToStep2() accepts optional jobOverride to bypass state; loadJobById() fetches from jobs table and auto-navigates to step 2; autoLoading banner shows while fetching by jobId; ?prefill auto-triggers parse on mount; ?jobId skips parse and jumps to step 2
  • •lib/ai.ts updated: callGroq() refactored into callGroqWithKey(apiKey, ...) shared implementation; callGroq() and callGroqSecondary() are thin wrappers; added generateTextSecondary() and generateJSONSecondary() that use GROQ_API_KEY_SECONDARY
  • •app/api/discover-jobs/route.ts updated: generateJSON replaced with generateJSONSecondary so job feed ATS scoring uses the secondary key and does not eat into the main key rate limit
  • •.env.local updated: GROQ_API_KEY_SECONDARY= added (fill in key)
May 17, 2026
Feature

Stripe billing - checkout, webhook, customer portal, and billing page

  • •lib/stripe.ts created: singleton getStripe() helper, SERVER-SIDE ONLY
  • •app/api/stripe/checkout/route.ts created: POST, requireAuth, creates Stripe Checkout Session (mode=subscription, success/cancel URLs, subscription_data.metadata.supabase_user_id)
  • •app/api/stripe/webhook/route.ts created: POST, no auth (Stripe calls it), verifies signature via constructEvent, handles customer.subscription.created/updated (upsert plan=pro) and customer.subscription.deleted (plan=free, status=cancelled), returns 200 immediately
  • •app/api/stripe/portal/route.ts created: POST, requireAuth, looks up stripe_customer_id from subscriptions, creates Stripe Customer Portal session, returns redirect URL
  • •app/(dashboard)/billing/page.tsx created: server component showing current plan badge, renewal date, upgrade/manage buttons, per-feature usage bars (tailoring, cover letters, auto-apply, strategy, networking, interview)
  • •components/BillingActions.tsx created: 'use client' UpgradeButton and ManageButton, each POSTs to the respective API route and redirects to the Stripe-hosted page
  • •components/Sidebar.tsx updated: added Billing nav item after Settings
  • •new env vars required: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_PRO_PRICE_ID, NEXT_PUBLIC_APP_URL
  • •new DB columns required: ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS stripe_customer_id TEXT, ADD COLUMN IF NOT EXISTS current_period_end TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS cancelled_at TIMESTAMPTZ;
May 17, 2026
Feature

Networking - contact discovery, outreach message generation, and tracker

  • •app/api/find-contacts/route.ts created: POST { company, role?, jobId? }, requireAuth, checkLimit('networking'), 5/day hard cap via route_logs; scrapes LinkedIn via Jina.ai with authwall detection, falls back to Google site:linkedin.com/in search; generateJSON extracts up to 8 contacts; optional job-relevance scoring if jobId provided
  • •app/api/generate-outreach/route.ts created: POST { contactName, contactTitle, contactCompany, messageType, contactLinkedinUrl?, jobId? }, requireAuth, checkLimit('networking'), buildUserContext + optional job context, generates connection_request (max 280 chars), cold_email (max 150 words), or follow_up (max 80 words) via Groq; saves to outreach_messages; tracks event
  • •app/(dashboard)/networking/page.tsx created: 3-section client page - (1) Find Contacts: company+role search populates contact cards with pre-fill buttons; (2) Generate Message: contact fields + message type + optional job selector, editable textarea with char counter for connection_request, Copy and Save buttons; (3) Outreach Tracker: table of saved messages with sent toggle, Follow-up action (pre-fills section 2 as follow_up type), and Delete
  • •components/Sidebar.tsx updated: Networking added to NAV_ITEMS between Strategy and Profile
May 17, 2026
Fix

Networking contact search - URL encoding, logging, and AI fallback

  • •app/api/find-contacts/route.ts: encodeURIComponent the full target URL before appending to r.jina.ai/ so LinkedIn query strings are routed correctly; added console.error logging of Jina response length and first 500 chars for debugging; added CAPTCHA detection to isUsableMarkdown; AI fallback now fires when contacts array is empty (not just when markdown is empty) - generates 3 realistic AI-suggested contacts with ai_generated:true; job-relevance scoring skipped for ai_generated source
  • •app/(dashboard)/networking/page.tsx: Contact interface gains ai_generated field; contactSource state tracks the search source from the API; ContactCard shows "AI suggested · verify manually" amber badge when ai_generated is true; amber disclaimer banner shown above contact cards when source is ai_generated
May 17, 2026
Feature

Cerebras AI for interview prep + full UI restyle

  • •lib/ai.ts: added generateTextCerebras and generateJSONCerebras using Cerebras OpenAI-compatible API (https://api.cerebras.ai/v1, model gpt-oss-120b) - separate rate limit from Groq
  • •app/api/interview-prep/generate/route.ts: switched from generateJSON (Groq) to generateJSONCerebras; removed forceSecondary logic; fixed JSON prompt format to use explicit object template; typed result as InterviewQuestions instead of any
  • •app/api/interview-prep/assess/route.ts: switched to generateJSONCerebras; fixed JSON prompt format; typed assessment as Assessment interface instead of any; simplified feedback keying (uses question text directly, consistent with client); removed qHash
  • •app/(dashboard)/interview/page.tsx: full restyle - landing page with Start session card and past sessions table (Company/Date/Progress columns); practice session with pill category tabs, redesigned question cards (question left, buttons right), Voice/Type toggle pills, "I don't know" link, typed textarea, assessment grid with Strengths/Improve columns and filler word badges
  • •.env.local: added CEREBRAS_API_KEY placeholder
  • •new env var required: CEREBRAS_API_KEY
May 17, 2026
Feature

Chrome Extension for job application auto-fill

  • •extension/manifest.json: Manifest V3 extension - activeTab, storage, host_permissions for all URLs
  • •extension/popup.html + popup.js: 280px popup with connect/main views; token saved to chrome.storage.local; fetches kit from /api/extension/kit; sends VANTAGE_FILL message to content script; shows filled count
  • •extension/content.js: fillForm() with nativeSetter pattern (React/Vue/Angular compatible); queryShadow() for Workday Shadow DOM; dispatches input/change/blur events; matches first name, last name, full name, email, phone, LinkedIn, cover letter, and application questions; never auto-submits
  • •extension/background.js: minimal service worker
  • •app/api/extension/kit/route.ts: GET route; Bearer token auth via service role client matching profiles.extension_token; returns firstName, lastName, fullName, email, phone, linkedin, coverLetter, answers; fetches cover letter and application_questions when job URL matches
  • •app/api/extension/token/route.ts: POST route; requireAuth; generates crypto.randomUUID() token; stores in profiles.extension_token + extension_token_created_at
  • •components/ExtensionTokenSection.tsx: client component on profile page; generate/regenerate token; one-time display of new token with copy button; masked display of existing token
  • •components/ProfileForm.tsx: phone field added (between full name and university); phone saved via updateProfile
  • •app/(dashboard)/profile/page.tsx: imports ExtensionTokenSection; fetches extension_token and extension_token_created_at from profiles; passes to component
  • •app/(dashboard)/actions.ts: Profile interface and updateProfile upsert include phone
  • •lib/userContext.ts: UserContext interface and fetchProfile query include phone
  • •SQL migration required: ALTER TABLE profiles ADD COLUMN IF NOT EXISTS extension_token text; ADD COLUMN IF NOT EXISTS extension_token_created_at timestamptz; ADD COLUMN IF NOT EXISTS phone text;
May 17, 2026
Update

Extension - custom div-based dropdown handler for Greenhouse and Lever

  • •extension/content.js: added async fillCustomDropdowns(kit) that finds role="combobox"/role="listbox"/class*="select__control" and similar triggers; clicks to open, waits 300ms, finds role="option" elements, clicks the matching option; handles work authorization, sponsorship, non-compete, compensation, referral source, and location questions
  • •extension/content.js: fillForm() converted to async, calls await fillCustomDropdowns(kit) after the Greenhouse second pass, returns { filled } object instead of a bare number
  • •extension/content.js: chrome.runtime.onMessage listener updated to return true and resolve with fillForm().then(sendResponse) so the async response is delivered correctly
May 17, 2026
Fix

Extension - fix dropdown bleeding and Lever location select

  • •extension/content.js fillCustomDropdowns: skips triggers inside phone fields (country-code pickers) via container querySelector for tel/phone inputs
  • •extension/content.js fillCustomDropdowns: dispatches Escape + 150ms pause before each trigger click to close any previously open panel
  • •extension/content.js fillCustomDropdowns: wait increased from 300ms to 500ms after opening a trigger before searching for options
  • •extension/content.js fillCustomDropdowns: option search scoped to the open panel (select__menu/dropdown-menu/listbox/SelectMenu) instead of document.body, preventing 244 country options from bleeding across all dropdowns
  • •extension/content.js fillCustomDropdowns: after clicking a matching option, waits 400ms then dispatches Escape to cleanly close the panel
  • •extension/content.js fillCustomDropdowns: Lever native-select second pass added at end of function - queries all document selects, matches label containing "location"/"where", fills via value + change event
May 16, 2026
Feature

Auto pre-fill profile from uploaded resume

Resume upload now extracts and pre-fills profile fields using AI parsing.

  • •app/api/parse-profile/route.ts created: POST route that takes rawText, calls generateJSON to extract full_name, university, graduation_year, years_experience, linkedin_url, skills, target_roles, then upserts only non-null fields into profiles table (preserves any existing values)
  • •components/ResumeUpload.tsx updated: after successful upload, fires a fire-and-forget POST to /api/parse-profile; on success calls router.refresh() so the profile page re-fetches updated data
  • •components/ProfileForm.tsx updated: added useEffect that syncs local field state when initialProfile prop changes, pre-filling only empty/default fields without overwriting user edits
May 16, 2026
Feature

Resume upload embedded in profile form with onboarding banner

Resume upload moved into the profile form with an onboarding welcome banner for new users.

  • •components/ResumeUpload.tsx updated: stripped standalone card wrapper and heading, added onUploadComplete prop that fires with rawText after a successful upload, removed internal parse-profile and router.refresh logic
  • •components/ProfileForm.tsx updated: renamed prop initialProfile to initialData, added isNew prop, welcome banner inside card when isNew, Resume section always rendered above form fields with handleUploadComplete that calls /api/parse-profile and updates empty fields directly without a page refresh
  • •app/(dashboard)/profile/page.tsx updated: removed standalone ResumeUpload section and old blue banner, passes initialData and isNew to ProfileForm
May 16, 2026
Feature

Application detail modal with ATS scores and tailored documents

Clickable application rows open a detail modal showing ATS scores, keywords, and document previews.

  • •app/api/applications/[id]/details/route.ts created: GET route that fetches the application, then uses Promise.all to fetch related documents and the latest ATS score, returns application, documents, and atsScore
  • •components/ApplicationDetailModal.tsx created: modal overlay with 800px panel; fetches details on mount; shows skeleton while loading; header with company, role, status badge, applied date; ATS section with overall_score, keyword_score, missing_keywords pills, suggestions list; tabbed document viewer for Tailored Resume and Cover Letter with Copy and Download PDF buttons; Escape key and overlay click to close
  • •app/(dashboard)/tracker/page.tsx updated: added selectedAppId state; each application row is now clickable; ApplicationDetailModal rendered at component root when selectedAppId is set
May 16, 2026
Fix

Status dropdown renders as portal to fix overflow clipping in tracker table

Status dropdown now renders in a portal to prevent clipping inside overflow containers.

  • •app/(dashboard)/tracker/page.tsx: replaced inline absolute-positioned status dropdown with a createPortal render into document.body; button onClick now calls getBoundingClientRect() to calculate absolute page position stored in dropdownPos state; dropdownPortalRef attached to the portal div for outside-click detection
May 16, 2026
Feature

Settings panel with account, appearance, resume, and danger zone sections

Settings page with account info, theme switching, resume upload, and account deletion.

  • •components/Sidebar.tsx updated: added Settings link to nav after Profile
  • •app/(dashboard)/settings/page.tsx created: server component that fetches user and profile, renders SettingsClient with userId, email, fullName
  • •components/SettingsClient.tsx created: 'use client' component with four sections — Account (email read-only + full_name save via /api/profile/update), Appearance (Dark/Light theme pills updating data-theme attribute and localStorage), Resume (inline ResumeUpload panel toggled by button, shows success message on complete), Danger Zone (delete button opens DeleteModal with type-DELETE confirmation, calls /api/account/delete then redirects to /login)
  • •app/api/profile/update/route.ts created: POST route, requireAuth, validateBody with full_name, updates profiles.full_name for the authenticated user
  • •app/api/account/delete/route.ts created: POST route, requireAuth, signs out via SSR client, deletes user via service-role client, returns ok with deleted true
  • •No new database tables or columns
  • •No new npm packages
May 16, 2026
Feature

Documents page with filter tabs, expandable rows, copy and download

Dedicated documents page with type filters, expandable content previews, and PDF download.

  • •app/(dashboard)/documents/page.tsx created: server component that fetches all documents with jobs join, passes to DocumentsClient
  • •components/DocumentsClient.tsx created: filter tabs (All / Tailored Resume / Cover Letter), each document row shows type badge + job title/company + date + Copy and Download PDF buttons, clicking row expands inline pre block with full content; EmptyState when no documents
May 16, 2026
Feature

Answer application form questions with AI

AI-powered question answering for job application forms, saved per application.

  • •app/api/answer-question/route.ts created: POST with jobId, applicationId, and question — requireAuth, validateBody, checkLimit, fetches job (ownership verified), buildUserContext, generateText with grounded-in-resume system prompt, saves to application_questions table, tracks event, returns saved question row
  • •app/api/answer-question/route.ts GET with applicationId param — returns all saved questions for the given applicationId belonging to the authenticated user, ordered by created_at ASC
  • •No new database tables — uses existing application_questions table
  • •No new npm packages
May 16, 2026
Feature

Apply Prep page — application kit, question answerer, submit checklist

All-in-one apply prep page with document kit, AI question answering, and a submit checklist.

  • •app/(dashboard)/apply/[jobId]/page.tsx created: 'use client' page; fetches job, documents, ats_score, and application via Supabase browser client in parallel on mount; loads existing application_questions via GET /api/answer-question
  • •Section 1 (Application Kit): 3-column grid showing latest tailored resume preview, cover letter preview, and ATS ScoreBadge; each card shows a link to /tailor when the corresponding asset is missing
  • •Section 2 (Question Answerer): textarea + Generate answer button; generated answers rendered in editable textareas with 500ms debounced PATCH for user_edited_answer; Saved indicator and Copy button per answer
  • •Section 3 (Submit Checklist): 5 manual checkboxes; when all checked, shows Mark as Applied button that updates status to applied and applied_date to today
  • •app/api/answer-question/[id]/route.ts created: PATCH route updating user_edited_answer on application_questions, ownership verified via user_id
  • •app/api/applications/[id]/route.ts updated: added applied_date to the list of PATCH-able fields
  • •No new database tables or columns
  • •No new npm packages
May 16, 2026
Feature

Job Feed — Adzuna-powered personalized job discovery with AI relevance scoring

Personalized job feed powered by Adzuna API with AI relevance scoring and save/dismiss controls.

  • •.env.local updated: added ADZUNA_APP_ID and ADZUNA_API_KEY placeholders
  • •app/api/discover-jobs/route.ts created: GET with optional refresh param; 24h cache check against job_feed_items; fetches from Adzuna for up to 3 target_roles (10 results each); deduplicates by external_job_id; scores all jobs in parallel with generateJSON; upserts to job_feed_items preserving is_saved and is_dismissed; returns non-dismissed items sorted by relevance_score DESC
  • •app/api/job-feed/[id]/route.ts created: PATCH with is_saved and is_dismissed fields, with ownership check
  • •app/(dashboard)/jobs/page.tsx created: 'use client'; loads on mount via GET /api/discover-jobs; Refresh button; employment_type filter dropdown; 2-col desktop / 1-col mobile grid; each card shows title, company, location, ScoreBadge, employment_type badge, AI reason, View job link, Tailor resume link, heart save toggle, dismiss button; saved items pinned to top
  • •components/Sidebar.tsx updated: removed comingSoon from Jobs nav item
May 15, 2026
Feature

App landing page at / with full marketing sections

Built the Vantage landing page with hero, problem, how-it-works, features, stats, pricing, and final CTA sections - fully static, inherits marketing layout.

What's included

  • •Full landing page at / covering all seven marketing sections.
  • •Hero: outcome-focused headline based on real validation data (75% of users got less than 5% response rate), dual CTAs, social proof line.
  • •Problem section: three data-backed pain point cards.
  • •How it works: three-step flow with connector line on desktop, collapses vertically on mobile.
  • •Features: 4-column grid of all eight product features, 2-column on tablet, single column on mobile.
  • •Stats: three validated data points rendered as large number cards.
  • •Pricing: two-column Free vs Pro layout at max-width 560px, Pro card uses --pro-border CSS variable for theme-aware highlight.
  • •Final CTA: centered section with primary button.

Files

  • •app/(marketing)/page.tsx created: static server component, all seven sections, no interactivity needed
  • •app/page.tsx deleted: removed conflicting boilerplate root page (route group page takes precedence)
  • •app/globals.css updated: added --pro-border CSS variable, scroll-behavior: smooth, and all .lp-* responsive utility classes
May 15, 2026
Feature

Shared marketing layout with nav, footer, and dark/light mode toggle

Added a sticky frosted-glass nav, responsive footer, and persistent dark/light mode toggle across all public marketing pages.

What's included

  • •Shared app/(marketing)/layout.tsx wraps all public pages (blog, changelog, landing page) with a consistent nav and footer.
  • •Sticky nav with frosted-glass background (blur(8px)), center links, Sign in, Get started button, and theme toggle.
  • •Dark/light mode toggle persists to localStorage under vantage-theme. Inline script in root <head> applies theme before first paint to prevent flash.
  • •Mobile responsive: center links and controls collapse into a slide-down drawer at 768px. Drawer traps focus and closes on Escape or outside tap.
  • •Footer with wordmark, tagline, four link columns (Product, Resources, Company, Legal), copyright, attribution, and social text links.
  • •Removed duplicate embedded nav bars from blog/page.tsx, blog/[slug]/page.tsx, and changelog/page.tsx.

Files

  • •app/(marketing)/layout.tsx created: server component, wraps children with MarketingNav and MarketingFooter
  • •components/marketing/MarketingNav.tsx created: client component - sticky nav, usePathname active detection, localStorage theme toggle, mobile drawer with focus trap
  • •components/marketing/MarketingFooter.tsx created: server component - footer with four link columns and social links
  • •app/layout.tsx updated: added <head> with inline FOUC-prevention script and suppressHydrationWarning on <html>
  • •app/globals.css updated: added --nav-bg / --nav-border CSS variables for both themes, added .mkt-nav-* and .mkt-footer-link utility classes with responsive breakpoint
May 15, 2026
Major Release

Launch 1 - ATS-first job search workspace

Launch 1 shipped ATS scoring, resume tailoring, cover letters, application tracking, and the first blog content system in one integrated workflow.

Launch 1 brings the core workflow from job discovery to tracked applications.

Included in this release

  • •ATS scoring for base and tailored resumes with score history and improvement tracking.
  • •Resume tailoring powered by job parsing and structured optimization suggestions.
  • •Cover letter generation aligned to job context and profile data.
  • •Application tracking dashboard with status updates and activity metrics.
  • •Blog content foundation with MDX support and frontmatter-based loaders.
May 15, 2026
Feature

Blog pages with MDX rendering, search, and category filtering

Blog infrastructure with MDX rendering, category filtering, and newsletter signup.

  • •app/(marketing)/blog/page.tsx created: server component, featured 2-up card layout, passes all posts to BlogClient
  • •app/(marketing)/blog/BlogClient.tsx created: client component handling newsletter signup, category filter, search, and post list
  • •app/(marketing)/blog/[slug]/page.tsx created: server component with generateStaticParams, generateMetadata, MDXRemote rendering, and "More from [category]" section
  • •components/mdx/MDXComponents.tsx created: custom renderers for h1 h2 h3 p code pre ul ol blockquote using CSS variables
  • •app/globals.css updated: added @plugin "@tailwindcss/typography" and .blog-prose overrides for CSS variable color integration
  • •new packages: @tailwindcss/typography, next-mdx-remote
May 15, 2026
Feature

Admin analytics dashboard

Admin-only dashboard showing revenue, user, retention, and product usage metrics.

  • •app/admin/page.tsx created: dynamic server component checking user against ADMIN_USER_ID, redirects non-admins to /dashboard, renders revenue, user, retention, product usage, growth, recent events, and route performance metrics via the Supabase service-role client
  • •.env.local updated: ADMIN_USER_ID placeholder added for local admin access
  • •No new database tables, columns, or indexes
  • •No npm packages installed
May 15, 2026
Feature

Contact page with public form and rate-limited API route

Public contact form with server-side rate limiting and Supabase persistence.

  • •app/(marketing)/contact/page.tsx created: server component, 560px centered layout, heading + subtext, renders ContactForm, shows email and location below a border
  • •components/marketing/ContactForm.tsx created: 'use client' form with name/email/subject/message fields, client-side validation, loading/success/error states, spinner in button while submitting
  • •app/api/contact/route.ts created: public POST route (no auth), validateBody, server-side email format check, service-role Supabase insert into contact_submissions, per-email hourly rate limit via SELECT count check
  • •new DB table: contact_submissions (id uuid pk, name text, email text, subject text, message text, created_at timestamptz) — no RLS, insert public, reads service-role only
  • •no new env vars
  • •no npm packages installed
Vantage
Your unfair advantage in job searching.
Product
FeaturesPricing
Resources
DocsBlogChangelogDiscord
Company
AboutFeedback
Legal
Privacy PolicyTerms of ServiceData Compliance
© 2026 Vantage, Inc.
Made by Ishvir Chopra