AI-assisted job-hunt platform. Human-approved pipeline: discover -> score -> approve -> draft -> send. LLM at decision points only, deterministic state machine owns the flow.
Implements the four views per the task card:
1. CV Editor (/cv):
- Profile form (name/email/phone/location/headline/summary) with save
- Section list editor: add/edit/delete sections with kind select,
title/org/dates, bullet list editor with per-bullet AI-assist
button calling POST /profile/sections/{id}/ai-assist, suggestions
shown with accept/dismiss
- Render CV button -> POST /profile/render-cv, shows URL link
2. Research (/research):
- Table of postings (GET /postings) with company/title/location/
source/fetched_at
- Add by URL input -> POST /postings
- Score button per row -> shows score result
3. Applications (/applications):
- Kanban board grouped by state (10 columns per data-model states)
- Cards show company/title/score
- HTML5 drag-and-drop between columns -> POST /applications/{id}/transition
- Optimistic update, revert on 409 with toast showing reason
- Click card to navigate to detail view
4. Application detail (/applications/:id):
- Posting info, state, score
- Artifacts list
- Cover-letter editor (textarea) -> save calls POST /applications/{id}/artifacts/
cover-letter, critique rendered as cards with severity color coding
- Approval widget: select artifact + action -> request approval ->
I confirm button -> Send button (disabled until confirmed; shows
409 errors as toasts)
Tech stack:
- Vue 3 + Vite + TypeScript (strict, noUnusedLocals/Parameters)
- Pinia for state (toast store)
- vue-router with lazy-loaded views
- Tailwind CSS configured locally (no CDN), PostCSS + autoprefixer
- API base from VITE_API_BASE defaulting to http://localhost:8000/api
- Typed API client module (src/api/index.ts) matching the contract
- Domain types (src/types/index.ts) from data-model.md
Tests (vitest, all passing):
- router.test.ts: router renders 3 tab links (CV, Research, Applications)
- Applications.test.ts: kanban groups cards by state from fixture
- ApplicationDetail.test.ts: Send disabled until confirmed; 409 error toast
Build: npm run build passes (vue-tsc --noEmit + vite build)
Tests: npm run test passes (4 tests, 3 files)
|
||
|---|---|---|
| apps | ||
| docs | ||
| packages | ||
| .gitignore | ||
| README.md | ||
Jobhunt Platform
An AI-assisted job-hunt platform where you stay in control. The system discovers jobs, scores them against your profile, drafts application material and prepares sends — but nothing external ever happens without your explicit approval.
Open source. Self-hosted. Single-user first, multi-user later.
Core ideas
- Approval gate, architecturally enforced. Every external action (send email, submit application) requires a server-side confirmed
Approvalreferencing the exact artifact hash. No approval row, no send. This is a hard constraint, not a style guide. - State machine owns the flow, LLM answers questions inside it. Pipeline transitions live in a table. The LLM scores, extracts, critiques and suggests — it never picks the next step. Deterministic orchestration, probabilistic judgment.
- Per-task model routing with token budgets. Cheap model for extraction/scoring, strong model for prose review, budgets enforced per task so the bill stays boring.
- The user drafts, the system reviews. Measured reality: human-drafted prose outperforms full AI drafts. Default cover-letter flow is user-writes, AI-reviews with tracked suggestions.
Architecture
apps/web Vue 3 + Vite + Tailwind — tabs: CV editor, Research, Applications (kanban), Application detail
apps/api FastAPI + PostgreSQL — REST API, state machine, scheduler, approval enforcement
packages/
llm-gateway/ Model routing, per-task budgets, structured JSON IO, retry/fallback policy
connectors/ Job source adapters -> normalized JobPosting (LinkedIn read-focused, jobindex, paste-a-URL)
artifacts/ CV + cover-letter generation: Jinja templates -> PDF (fpdf2), versioning, hashing
docs/ ADRs, data model, API contract, worker task cards
Pipeline
discovered -> scored -> approved -> drafting -> sent -> interviewing -> offer -> closed
\-> rejected (by user) \-> expired
External comms are only possible from approved/drafting states, and only with a matching confirmed Approval.
Quick start (POC)
cp .env.example .env # add LLM provider keys
docker compose up -d postgres
cd apps/api && uv venv .venv && . .venv/bin/activate && uv pip install -e .
pytest # backend tests
uvicorn app.main:app --reload
cd apps/web && npm install && npm run dev
Status
POC scaffolding in progress. See docs/ for the design.