11 KiB
S01 — Research
Date: 2026-03-24
Summary
S01 owns R001 and R002 directly, and it materially supports R010 because imported correspondence becomes part of the job timeline and later follow-up context. The codebase already has a complete Gmail OAuth path, a Gmail message listing endpoint, single-message and thread import endpoints, and a per-job correspondence UI. The gap is not missing Gmail plumbing; it is that matching is still mostly manual. The backend returns raw Gmail search results, while the frontend (job-tracker-ui/src/components/Correspondence.tsx) applies a lightweight client-side score based only on the freeform query, snippet text, and already-imported subjects. That does not use the actual job/company context strongly enough to satisfy the “less manual cleanup” bar in R002.
The best approach is to keep the existing OAuth/import flow and add a job-aware matching layer rather than replacing the Gmail integration. In practice that means: enrich backend candidate discovery around a specific JobApplication, return grouped thread/message suggestions with explicit match reasons/confidence inputs, and then update the correspondence dialog to present those suggestions first. This follows the current architecture cleanly and preserves the no-auto-send boundary from D002. The React side should keep async work consolidated instead of scattering additional fetches across effects; the loaded react-best-practices skill is relevant here, especially async-parallel and client-event-listeners.
Recommendation
Add a dedicated job-scoped Gmail matching surface on top of the existing endpoints instead of trying to make the current generic /api/gmail/messages search UI smarter only in the browser.
Recommended shape:
- Backend: add a job-aware endpoint in
JobTrackerApi/Controllers/GmailController.csthat acceptsjobApplicationIdand optional overrides, loads the job + company context, builds Gmail queries fromJobTitle,Company.Name,Company.RecruiterEmail, recruiter name, and recent imported correspondence, then returns ranked message/thread candidates with match reasons and enough metadata for import decisions. - Persistence: if the planner wants durable thread-aware behavior, extend
Models/Correspondence.csbeyondExternalMessageIdto also persist at leastExternalThreadIdand raw sender/recipient metadata. This is the cleanest way to support downstream S03 reply/follow-up context without re-deriving it later. - Frontend: refactor
job-tracker-ui/src/components/Correspondence.tsxso the Gmail tab consumes enriched API data instead of doing primary ranking locally.JobDetailsDialog.tsxalready loads the full job; passing the job or a reduced job-context prop intoCorrespondenceis cheaper than forcing the Gmail tab to rediscover job facts.
Why this approach:
- It uses the existing Gmail OAuth/token flow unchanged.
- It moves matching logic to the backend where job context, dedupe checks, and future heuristics are easier to test.
- It avoids over-investing in fragile client-only heuristics.
- It creates a natural seam for S03, where better thread metadata and message provenance will matter again.
Implementation Landscape
Key Files
JobTrackerApi/Controllers/GmailController.cs— current Gmail API surface. Has/status,/connect-url,/messages,/import, and/import-thread. Import endpoints already attach messages toJobApplicationand dedupe byCorrespondence.ExternalMessageId, but discovery is still generic and not job-aware.JobTrackerApi/Services/GmailOAuthService.cs— Gmail OAuth/token refresh and Gmail API access.ListMessagesAsynccurrently calls the Gmail list endpoint and then does an N+1 sequence ofGetMessageAsynccalls to hydrate summaries. There is no thread-specific fetch API, no job-aware query builder, and no ranking/match-reason contract here yet.job-tracker-ui/src/components/Correspondence.tsx— the main S01 frontend surface. It opens the Gmail dialog, loads/gmail/status, loads/gmail/messages, groups bythreadId, and sorts usingscoreMessage(...). Current suggestions come from existing correspondence subjects, not from job/company/recruiter context.job-tracker-ui/src/components/JobDetailsDialog.tsx— already loads the fullJobApplicationrecord and hosts the Correspondence tab. This is the easiest place to pass job context intoCorrespondenceinstead of refetching it inside the Gmail tab.job-tracker-ui/src/types.ts— frontend contracts forGmailStatus,GmailMessageSummary, andCorrespondenceMessage. Any enriched matching response or persisted metadata expansion needs updates here.Models/Correspondence.cs— currently storesFrom,Subject,Channel,ExternalMessageId,Content, andDate. NoThreadId, no original Gmail sender/recipient fields, and no match/debug metadata.Data/JobTrackerContext.cs— EF relationships and ownership filters.JobApplication,Company, andGmailConnectionare user-scoped via query filters; new Gmail-matching endpoints should continue loading jobs through this context rather than bypassing ownership.Models/Company.csandModels/JobApplication.cs— hold the matching signals that the current Gmail UI ignores:Company.Name,RecruiterEmail,RecruiterName,JobTitle,JobUrl,ShortSummary, and existing correspondence/timeline relationships.JobTrackerApi/Controllers/CorrespondenceController.cs— current create/list/delete API for job-linked messages. If S01 persists extra Gmail metadata, this contract may need to expose it to the UI and timeline.JobTrackerApi/Controllers/JobApplicationsController.cs— downstream dependency surface. It already readsCorrespondencesfor follow-up drafting and timeline assembly, so better message/thread metadata here directly helps S03.JobTrackerApi/Program.cs— important migration/backfill guardrail. The app manually ensures legacy SQLite/MySQL columns such asCorrespondences.Subject,Channel, andExternalMessageId. If S01 adds new persistence columns, these compatibility blocks must be updated alongside the EF migration.JobTrackerApi.Tests/GmailControllerTests.cs— only covers the empty-thread import validation case today. Good starting point, but far below the verification level needed for S01.job-tracker-ui/src/job-details-generated-drafts.test.tsx— representative React test style: mockapi, renderJobDetailsDialog, assert visible tab behavior. Follow this pattern for new Gmail suggestion/import UI tests.
Build Order
-
Decide and lock the backend contract first.
- Prove what a “smarter match” response looks like: message/thread grouping, rank/confidence, reasons, imported/already-linked flags, and import actions.
- This is the riskiest part and unblocks everything else.
-
Implement job-aware matching in
GmailController+ supporting service/helpers.- Load
JobApplicationwithCompany. - Build candidate Gmail queries from job/company/recruiter data.
- Merge/dedupe results by message id or thread id.
- Compute match reasons server-side.
- Keep existing
/importand/import-threadbehavior unless the new contract proves they need richer return payloads.
- Load
-
Only after the contract is stable, refactor
Correspondence.tsx.- Replace
scoreMessage(...)as the primary ranking engine with server-provided ranking/reasons. - Pass job context from
JobDetailsDialog.tsxrather than introducing another job fetch in the correspondence tab. - Keep manual query override/search available as a fallback, not the primary UX.
- Replace
-
Then extend persistence if needed for thread continuity.
- Add correspondence metadata only if the chosen backend contract needs it for dedupe, import clarity, or future reply context.
- If added, update model, migration, and
Program.cscompatibility shims together.
-
Finish with tests.
- Backend tests for matching/import behavior first.
- Frontend tests for the new Gmail suggestion UI second.
Verification Approach
- Backend unit/integration tests:
dotnet test JobTrackerApi.Tests- Add tests for: job-aware candidate endpoint contract, dedupe behavior for already-imported messages, ownership-scoped job lookup, and thread import summary results.
- Frontend tests:
npm test -- --watch=falsefromjob-tracker-ui- Add React tests covering: Gmail tab rendering ranked suggestions, showing match reasons, import button states, and fallback/manual search behavior.
- Contract/manual verification:
- Open a job in
JobDetailsDialog→ Correspondence tab. - Confirm Gmail connection state still works.
- Confirm the Gmail tab now shows job-relevant suggestions before freeform searching.
- Import a single message and a full thread; verify the job timeline/correspondence list updates and duplicates are skipped.
- Open a job in
- If persistence changes land:
- Verify schema startup still succeeds on existing dev DBs because
JobTrackerApi/Program.cslegacyEnsureColumn(...)blocks are easy to miss.
- Verify schema startup still succeeds on existing dev DBs because
Constraints
Data/JobTrackerContext.csapplies ownership query filters toCompany,JobApplication, andGmailConnection. Any new Gmail matching endpoint must keep loading through EF-scoped entities, not raw unfiltered ids.JobTrackerApi/Services/GmailOAuthService.cscurrently exposes only message list/detail methods. There is no reusable thread-fetch or search-aggregation abstraction yet.JobTrackerApi/Program.cscontains manual schema repair code for SQLite/MySQL. Adding correspondence metadata requires updating both EF migration artifacts and these runtime compatibility paths.Models/Correspondence.csdoes not currently preserve Gmail thread identity or raw sender/recipient fields, which limits downstream thread-aware UX.
Common Pitfalls
- Leaving ranking in the browser —
Correspondence.tsxcan polish server results, but if the primary intelligence stays inscoreMessage(...), S01 will remain query-driven and fragile. - Adding columns without updating
Program.cs— this repo relies on startup-timeEnsureColumn(...)logic for legacy/dev databases; migration-only changes are incomplete here. - Duplicating job fetches in the dialog tree —
JobDetailsDialog.tsxalready owns the job record. Per thereact-best-practicesguidance (async-parallel,client-event-listeners), keep async fetching consolidated and avoid adding more dialog-level waterfalls or duplicated global listeners. - Treating thread import as enough without thread metadata — importing all messages in a thread helps today, but without persisting thread identity the app still cannot reason clearly about thread continuity later.
Open Risks
- Gmail search quality may still be noisy even after better query construction; the planner should expect one iteration on ranking heuristics once real data is exercised.
ListMessagesAsyncis sequential and could become noticeably slow if the new matching flow issues multiple Gmail searches per job. If that happens, batching/parallelization inside the Gmail service becomes part of S01, not a later optimization.
Skills Discovered
| Technology | Skill | Status |
|---|---|---|
| React | react-best-practices |
available |
| ASP.NET Core | openai/skills@aspnet-core |
installed |