Skip to content

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

Landed

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


What it does

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.

Insights


Running it

./run.sh --seed     # build, install, seed demo data, serve

Then 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.

Development

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 dev

Vite serves the UI on :5173 and proxies /api to the backend on :8000.

Tests

cd backend && pytest -q            # 39 tests
pytest -q --cov=app                # ~95% coverage

How it is built

backend/
  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.


Design decisions

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.


Licence

MIT

About

Job search command center with Kanban board, funnel analytics, and offline skill-gap matching.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages