Walkthrough

A real CV, through the real thing.

Every image below is a capture of the running application — the production Next.js build talking to the live FastAPI process, holding the real 800-role corpus and the trained model. Playwright drove the journey; nothing is mocked, stubbed or drawn.

How these were made, so you can discount them properly.

A script drove a headless browser through the application end to end and screenshotted each step. The API was the real one: 800 roles loaded, the joblib model loaded, SQLite behind it.

Two résumés were used. One is the author's own PDF, parsed by the same code path as any upload. The other belongs to "Alex Rivera", who does not exist — a file written for this capture so the shortlist has more than one candidate on it. The corpus itself is a purpose-built 800-record dataset, not scraped listings.

Explanations are switched off in these runs. The "Write explanations" toggle is unchecked, so what you see is scoring only. That path calls a language model and adds roughly 30–60 seconds per résumé, which is a poor fit for a capture script and would not have shown you anything the score breakdown does not.

The address bar in each frame shows the deployed URL. The captures were taken against a local production build of the same commit.

Stage 1 — Discover

Find out what you are matching against

Before a single CV is uploaded, the corpus has to be inspectable. Every figure on these pages is served by GET /stats and GET /jobs, so the interface and the data cannot disagree.

01

Landing

The counts here are not written into the page. They come from /stats on load — 800 roles across 27 countries, 60 companies, 654 distinct skills. An earlier version of this page claimed "3,000+ jobs" against a corpus that had never held that many.

https://recruiter-pro-nine.vercel.app/
Recruiter Pro landing page
02

Browse the corpus

Eight categories, three work models, six seniority levels — and the filter values are read from the corpus itself via /jobs/facets rather than hardcoded, so a filter can never offer an option that returns nothing.

https://recruiter-pro-nine.vercel.app/jobs
The jobs page listing roles from the corpus
03

Search, done server-side

Typing kubernetes filters across title, company, city and skill on the server. This search box previously existed and did nothing: /jobs accepted only skip and limit, so search=nurse returned a byte-identical page to no search at all.

https://recruiter-pro-nine.vercel.app/jobs?search=kubernetes
Server-side search filtering the corpus for kubernetes
04

One role, in full

The description keeps its four-section structure because the element preserves newlines rather than collapsing them. Required and preferred skills are separated, and a CV can be scored against this one role alone — the same components, without the ranking.

https://recruiter-pro-nine.vercel.app/jobs/ENG-0001
Job detail page for a Machine Learning Engineer role
Stage 2 — Analyse

Score one résumé against all 800 roles

One pass over the whole corpus, not a job at a time. Parsing, extraction, five weighted components per role and one vectorised model call for the lot.

05

Upload

PDF, DOCX and TXT, capped at 10 MB — and the cap stated here is the one the API enforces. The file's real leading bytes are checked against its extension before any parser touches it, so a .exe renamed .pdf is refused rather than parsed.

https://recruiter-pro-nine.vercel.app/upload
The upload page with its drag and drop area
06

Ranked, with the arithmetic shown

A real 161 KB PDF, scored against all 800 roles. The header reports what actually happened — the corpus size and the elapsed time, measured rather than estimated. Top match 75, one strong, nine worth a look, average 71.5%.

Each card carries the matched skills and the missing ones, resolved through the vocabulary rather than by string equality, so "React" and "ReactJS" are the same skill and "Java" and "JavaScript" are not.

https://recruiter-pro-nine.vercel.app/upload
Ranked match results for a real CV against the whole corpus
07

Where the number came from

Expanding a match shows each component separately and then the part that matters most: a stacked bar of contributions, each segment the component's score multiplied by its weight. A candidate at 74 because their skills are excellent is a different candidate from one at 74 because everything is mediocre, and four independent meters make you do that arithmetic yourself.

The segments sum to the total, and a test says so. A stacked bar built on weights that have drifted does not look wrong — it looks right, which is why the invariant is asserted rather than assumed.
https://recruiter-pro-nine.vercel.app/upload
An expanded match showing the score breakdown and composition bar
Stage 3 — Triage

Decide, and keep the decision

08

Shortlist

Each candidate's strongest five roles, banded by score, with accept and reject decisions that persist. A shortlist is the artefact a recruiter hands to someone else, so this is the view that exports to CSV — the filtered view, because the filter is the shortlisting.

This page had a bug, found while capturing these screenshots. It promised "each candidate's top 5 roles" and delivered top 5 rows. Match history keeps every run, so uploading the same CV twice wrote a second row for the same candidate-and-role pairing, and the board filled with duplicate cards identical except for their timestamps — 278 stored rows collapsed to just 31 distinct pairings.

It now collapses on candidate and job id, keeping the highest-scoring row. Deliberately job id and not job title: two companies both advertising "Systems Engineer" are two roles a recruiter must see separately, and this corpus contains exactly that. The screenshot below is the fixed build.

https://recruiter-pro-nine.vercel.app/shortlist
The shortlist page with candidates banded by score
09

History

Every match ever run, newest first, written to SQLite in a single transaction per upload rather than a connection per row. Unlike the shortlist this view does not collapse repeats — a second run of the same CV is a real event and belongs in the record.

https://recruiter-pro-nine.vercel.app/history
Match history listing stored matches
10

Dashboard

The aggregate view over what has been scored so far. The sidebar carries a scoring indicator reading hybrid or rules-only, because the model failing to load is otherwise invisible — rule-based scores are just as plausible-looking as hybrid ones.

https://recruiter-pro-nine.vercel.app/dashboard
The dashboard summarising scored matches
11

At 390 pixels

The same results on a phone: navigation becomes a drawer below lg, and no page scrolls horizontally at 375px. Captured at a real mobile viewport, not a narrowed desktop one.

recruiter-pro-nine.vercel.app
Match results rendered on a mobile viewport

Three bugs were found by doing this. Walking the real application for screenshots surfaced the shortlist duplication above; a name-extraction fault that stored a candidate called "Berlin, Germany", because a city in the address blocklist vetoed the line "Alex Rivera"; and a README performance figure measured on a fixture a fifth the density of a real résumé.

Each was reproduced, given a test that failed first, then fixed — and these screenshots were re-captured against the corrected build. The test suite went from 529 to 544 passing.