Finish the deferred/open items on the applications tracker: - Cover-letter upload re-enabled. Fix the storage blocker by deriving the key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE) instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add coverLetterUrl/coverLetterName columns + Documents-section upload/remove. - Contacts editor in the detail sheet (add/edit/remove, keyed per app). - Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more"). - Extract new Lingui messages across locales. - Guard coverLetterUrl to http(s)/relative at the API boundary. Squash the five branch-only application-table migrations (create -> +tags -> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via drizzle-kit generate. Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
9.1 KiB
Applications Tracker — Roadmap & Status
Status legend: ✅ done · 🟡 in progress · ⬜ not started · 🧊 deferred (intentional, revisit when needed)
A job-application tracker built inside Reactive Resume. Each application points at the live
resume the user sent (resumeId), which is why it lives in-product rather than a generic
tracker. Built from the claude.ai/design prototype "Applications Tracker.dc.html".
Owner dirs: packages/db/src/schema/applications.ts, packages/schema/src/applications/,
packages/api/src/features/applications/, apps/web/src/features/applications/,
apps/web/src/routes/dashboard/applications/.
Phase 0 — Core slice ✅ (shipped)
The working vertical slice: data model, CRUD, board, add/detail panels. Zero new deps.
- ✅ DB:
applicationtable (user FK cascade,resumeIdFK set-null, JSONBcontacts/activity, follow-up + AI-reservation columns). Migration20260705090711_third_hercules. - ✅ Schema:
applicationStatusSchema,STAGES(value/label/color),contactSchema,activityEventSchema,aiMetadataSchema. - ✅ API: oRPC
applications.{list,getById,create,update,addNote,delete}—protectedProcedure,userId-scoped via onerequireOwned;updateauto-logs stage-change activity. - ✅ Web: sidebar nav item;
/dashboard/applicationsroute (empty state ↔ board). - ✅ Board: dnd-kit drag across 6 fixed stages, optimistic move + rollback toast.
- ✅ Add slide-over: manual entry + link a live Reactive Resume; native date follow-up.
- ✅ Detail slide-over: key facts, stage stepper, linked resume, contacts, follow-up, activity timeline + add-note, archive/delete.
- ✅ Unit test: activity-logging path (
service.test.ts). Typecheck / boundaries / biome clean.
Known small gaps — now closed (✅):
- ✅ Lingui messages extracted (
pnpm --filter web lingui:extract) so the new<Trans>/tstrings are translatable. - ✅ Board caps rendered cards per column (
COLUMN_PAGE_SIZE = 50) with a "Show N more" button, so a stage with hundreds of applications doesn't mount hundreds of draggable nodes. Server-side paging still unneeded (list payload is small). - ✅ Contacts are editable from the Detail panel (
ContactsEditor): add name/role/label, remove, persisted via APIupdate's existingcontactsfield.
Phase 1 — Table view + Insights ✅ (shipped)
- ✅ Table view — paginated table (25/page), row selection + bulk actions (move stage, add tag, archive, delete). Added a wrapped
Checkboxinpackages/ui; hand-rolled table (no@tanstack/react-tabledep). Board/Table/Insights toggle via URLviewsearch param. - ✅ Insights view — stat tiles, pipeline funnel (conversion per stage), source bars, and a shareable funnel-flow SVG with PNG export (canvas, no chart lib — kept the zero-dep property). Server aggregation
applications.stats(per-stage/per-source counts); funnel/tiles derived client-side viacomputeInsights()(unit-tested). - ✅ Tags:
tags text[]column + filter UI + bulk "add tag" +applications.tagsdistinct list. - ✅ Archived-toggle so archived rows can be reached and unarchived (closes the archive loop).
Phase 2 — Campaigns + import + uploads ✅ (shipped)
- ✅ Campaigns — the
campaigntext field is now first-class: set on create (with a native<datalist>autocomplete of existing campaigns), filter on board/table, per-campaign Insights scoping, and anapplications.campaignsdistinct-with-counts endpoint. (Kept it a text field, not a table — grouping/filtering/insights work without the join-table overhead.) - ✅ CSV import —
Import CSVopens a slide-over: paste rows or upload a.csv, a zero-dep parser (csv.ts, unit-tested) maps aliased headers (Company/Role/Stage/Salary/Source/Tags/…), previews recognized columns + ready/skipped counts, then bulk-creates viaapplications.import(max 500, each row gets acreatedactivity event). Verified end-to-end in the browser. - ✅ Cover-letter upload —
coverLetterUrl/coverLetterNamecolumns; the detail panel's Documents section attaches a PDF via the existingorpc.storage.uploadFile, shows a download link, and supports removal.
Phase 3 — AI agent integration ✅ (shipped, verified live)
All four applications.ai.* procedures are implemented (packages/api/src/features/applications/ai.ts), resolving the user's default provider via aiProvidersService.getDefaultRunnable → getModel → generateText, with tolerant JSON parsing. Rate-limited via aiRequestRateLimit.
- ✅ Job-posting autofill — paste a URL → server fetches + strips the page → LLM extracts company/role/location/salary/jobDescription → prefills the Add form. (
jobDescriptionis now a user-editable field so it persists for match/tailor.) - ✅ Resume↔job match score — scores the linked resume vs the job description, persists
matchScore+ gaps inaiMetadata. Rendered as a fit ring in the redesigned copilot. - ✅ Tailor resume — one-shot: duplicates the linked resume, regenerates a job-tailored
summary, links the copy to the application, logs a timeline event. (No agent/REDIS dependency.) - ✅ Draft cover letter / follow-up — generates from application + resume context; shown inline with copy/dismiss.
- ✅ UX: the AI section was redesigned into an "Application Copilot" module — a resume-fit ring (band-colored) as the signature element + a capability menu (icon + title + description) instead of plain buttons.
Verified live against a real OpenAI provider (autofill, match score, tailor resume).
Extra polish shipped alongside (from review + user requests)
- ✅ Edit an application — the Add sheet became
ApplicationFormSheet, edit-capable; reachable from the detail-panel "Edit" button and the context menu. - ✅ Context menus — kebab "⋯" on board cards (hover) and table rows: Edit / Move to ▸ / Archive / Delete (
application-actions-menu.tsx). - ✅ Review fixes: bulk stage-moves log activity; single-statement note append; "Mark rejected" + "no results" states; CSV BOM strip + 500-row cap + file-input reset.
✅ Cover-letter upload (re-enabled)
Un-deferred. The blocker was storage/service.ts hardcoding a .jpeg key for every upload, so a PDF was read back as an image. Fixed by deriving the key extension from the content type the router already passes (buildFileKey + EXTENSION_BY_CONTENT_TYPE) — images stay .jpeg (unchanged for avatars), PDFs get .pdf and the static handler's existing .pdf force-download path serves them correctly. Re-added coverLetterUrl/coverLetterName columns (migration 20260705125353_yielding_star_brand), DTO editable fields, and a Documents-section upload/download/remove in the detail sheet (PDF-only, best-effort storage.deleteFile on remove). Link-to-Reactive-Resume remains the primary document feature; the cover letter sits alongside it.
(historical) Phase 3 plan — reservations in place, no model wired yet
Reservations already shipped: schema fields (sourceUrl, jobDescription, matchScore,
aiMetadata) and stubbed applications.ai.* procedures that throw NOT_IMPLEMENTED
(packages/api/src/features/applications/ai.ts). Each stub below just needs its handler
implemented against @reactive-resume/ai and the existing agent/thread tables
(agentThread, agentAction in packages/db/src/schema/agent.ts).
- ⬜ Job-posting autofill (
applications.ai.autofill) — fetchsourceUrl(or accept pasted text) → LLM extracts company/role/location/salary/jobDescription → return a prefill object for the Add form; persist raw extraction inaiMetadata. UI: enable the disabled "Auto-fill" button inadd-application-sheet.tsx. - ⬜ Resume ↔ job match score (
applications.ai.matchScore) — compare the linked resume's data againstjobDescription→ writematchScore(0–100) + a gap list intoaiMetadata. UI: surface a score badge on the card + a gaps section in the Detail panel. - ⬜ Tailor resume for this job (
applications.ai.tailorResume) — spawn an agent thread that duplicates the linked resume and edits it for the posting (reuse the agent JSON-patch pipeline); link the newresumeIdback to the application and log a timeline event. - ⬜ Cover-letter / follow-up drafting (
applications.ai.draftMessage) — generate a cover letter or recruiter follow-up from application + resume + contact context. UI: the disabled "AI actions" block inapplication-detail-sheet.tsx. - ⬜ Rate-limiting & provider check — reuse
aiRequestRateLimit(already exists) and gate on a configured AI provider (aiProviderstable) before calling any of the above.
Suggested order: autofill (highest user value, self-contained) → match score → draft message → tailor resume (most complex, depends on the agent pipeline).
Verification (each phase)
pnpm --filter @reactive-resume/api test+typecheck,pnpm --filter web typecheck,pnpm exec turbo boundaries,pnpm check.- DB changes:
dotenvx run -f .env.local -- pnpm db:generate→ review SQL →pnpm db:migrate. - Manual:
dotenvx run -f .env.local -- pnpm dev(:3000) → exercise the new surface end-to-end.