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)
packages/artifacts:
- render_cv_pdf(profile, sections) -> bytes: data-driven CV PDF generation
using fpdf2 with bundled DejaVuSans TTF for unicode (Swedish chars tested).
Jinja2 template for layout data prep, adapted from build_cv.py approach.
- render_cover_letter(text, profile) -> bytes: simple cover letter PDF.
- hash_bytes(b) -> str: sha256 hex digest.
- next_version(existing) -> int: version numbering helper.
- 16 tests, all passing: PDF validity, Swedish characters, hash stability,
cover letter rendering, hash correctness, version logic.
packages/llm-gateway:
- Async-first Gateway class with provider config from env.
- Mock mode default when no API key env present (deterministic canned outputs
per task name, defined in mock.py).
- Telemetry sink injectable (async or sync callable, receives TelemetryRow).
- Budget guard raises BudgetExceeded BEFORE any provider call is made.
- Retry policy: max 2 retries on 429/5xx, then fallback provider for STRONG
tasks only. CHEAP tasks never use fallback (paid provider protection).
- Schema validation via jsonschema; SchemaValidationError on mismatch.
- Task class routing: CHEAP (score, extract, cv_assist) vs STRONG (critique,
cl_critique, research). Model routing per task class.
- Paid provider detection heuristic; warns on paid fallback config.
- 31 tests, all passing: mock determinism, schema pass/fail, budget guard
(mock + real mode), telemetry sink (async/sync/none), config from env,
provider calls with mocked HTTP (retry, fallback, no-fallback-for-cheap).
docker-compose.yml:
- postgres:16 service, user/pass/db = jobhunt, host port 5433->5432,
named volume jobhunt_pgdata, healthcheck.
.env.example:
- DATABASE_URL, LLM provider config (primary + fallback), task budgets,
API and web settings.