A job search command center. Track applications on a board, see where you actually stall, and check any posting against your skills before you spend an hour on the application.
Built because a spreadsheet tells you what you applied to, but not that your response rate collapsed the week you started skipping the cover letter, or that eight applications have been sitting unanswered long enough to be worth a follow-up.
Board. Drag applications through Saved, Applied, Screening, Interview, Offer and Rejected. Cards flag themselves when they have been waiting on a reply for a week or more, and the follow-up filter shows only those.
Insights. Response rate, median days to first reply, interview rate, and a funnel showing conversion between stages. All of it is computed from your stage history rather than from where cards sit right now, so a rejection does not erase the fact that a company interviewed you.
Skill match. Save your skills once, then paste any job description to see what it asks for that you have, and what it asks for that you do not.
./run.sh --seed # build, install, seed demo data, serveThen open http://127.0.0.1:8000. Drop --seed to start empty.
Requires Python 3.10+ and Node 20+. The frontend builds into the backend's
static/ directory, so production is one process on one port with no separate
web server.
Two processes, with hot reload on both:
# terminal 1
cd backend && pip install -e ".[dev]" && uvicorn app.main:app --reload
# terminal 2
cd frontend && npm install && npm run devVite serves the UI on :5173 and proxies /api to the backend on :8000.
cd backend && pytest -q # 39 tests
pytest -q --cov=app # ~95% coveragebackend/
app/
models.py applications, status events, skill profile
analytics.py funnel, response rates, weekly activity
skills.py taxonomy, extraction, gap analysis
routers/ HTTP layer
frontend/
src/
views/ Board, Insights, Analyzer
components/ Drawer, icons
FastAPI, SQLAlchemy and SQLite on the back. React and Vite on the front, with no UI framework, no state library and no drag-and-drop library.
Status changes are events, not a column.
An application could carry a single status field, but then moving a card
destroys the history, and every question worth asking about a job search is a
question about history. Transitions are appended to a status_events table and
the current status is derived from the latest one. That costs one table and buys
the entire Insights page.
The funnel counts stages ever reached. An application now sitting at rejected still counts as having reached interview if it ever did. Counting only current status would make a company that interviewed you look identical to one that ghosted you, which is the opposite of useful.
A rejection counts as a response. Response rate measures whether anyone replied at all, including to say no. Lumping "nobody read this" together with "they read it and passed" hides which problem you actually have: the first is a resume and targeting problem, the second is a fit problem, and they need different fixes.
Skill matching is a curated taxonomy, not an LLM. The task is recognition, not comprehension: find which known technologies appear in a posting. A local taxonomy does that instantly, offline, for free, and identically every time, which matters when you are comparing results across postings. An LLM would add latency, cost, an API key requirement and non-determinism to a problem that has none of those.
The taxonomy resolves aliases, so postgres, PostgreSQL and psql collapse
to one skill, and longer names win over their substrings so React Native does
not also register React.
Known limitation: a few skill names are ordinary English words. Go is the
worst, and matching it means "on the go" in a benefits paragraph registers as
the language. Dropping the bare alias instead would make "experience with Go"
invisible, which is the more damaging failure for a tool whose whole job is
spotting gaps. The output prompts a human reading a posting; it is not an
automated decision.
Board moves are optimistic. The card moves immediately and the request follows. Waiting on a round trip to see a card move makes a board feel broken. A failed request puts the card back and surfaces the error.
Single user, no auth. This is a tool one person runs for their own search. Adding accounts, sessions and a users table would not make it better at the thing it does.
MIT

