--- id: S01 parent: M001 milestone: M001 provides: - Job-scoped Gmail matching and import now run from the job workspace with backend-owned ranking reasons, duplicate-aware import contracts, persisted Gmail thread metadata, and linked-thread refresh that imports later replies into the same job. affects: - S02 - S03 - S04 - S05 key_files: - JobTrackerApi/Controllers/GmailController.cs - JobTrackerApi/Services/GmailOAuthService.cs - JobTrackerApi.Tests/GmailControllerTests.cs - Models/Correspondence.cs - JobTrackerApi/Controllers/CorrespondenceController.cs - job-tracker-ui/src/components/Correspondence.tsx - job-tracker-ui/src/components/JobDetailsDialog.tsx - job-tracker-ui/src/types.ts - job-tracker-ui/src/correspondence-gmail-import.test.tsx key_decisions: - Keep Gmail continuity bounded to known `ExternalThreadId` values for one job via `POST /api/gmail/refresh-linked-threads` instead of inbox-wide Gmail watch/history infrastructure. - Treat the backend as the source of truth for Gmail candidate ranking, duplicate visibility, and linked-thread refresh counts so the workspace UI stays explanatory without re-implementing Gmail heuristics in React. patterns_established: - Gmail-derived correspondence is first-class job history: imported rows persist external message/thread ids plus sender/recipient labels and can be rendered directly in the timeline/workspace. - Linked-thread continuity is pull-based and duplicate-safe: refresh reads already-linked thread ids for one owned job, skips known external message ids, and imports only new Gmail replies into that same job. - The workspace distinguishes ranked import suggestions from already-linked live threads, with automatic one-shot refresh per loaded job/thread-set and an explicit manual refresh action. observability_surfaces: - GET /api/gmail/status - GET /api/gmail/job-candidates - POST /api/gmail/import - POST /api/gmail/import-thread - POST /api/gmail/refresh-linked-threads - persisted Correspondence.ExternalMessageId / ExternalThreadId / ExternalFrom / ExternalTo - JobTrackerApi.Tests/GmailControllerTests.cs - job-tracker-ui/src/correspondence-gmail-import.test.tsx drill_down_paths: - .gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md - .gsd/milestones/M001/slices/S01/tasks/T02-SUMMARY.md - .gsd/milestones/M001/slices/S01/tasks/T03-SUMMARY.md duration: ~1 slice closure session + prior executor task work verification_result: passed completed_at: 2026-03-24T11:54:52+01:00 --- # S01: Smarter Gmail import and matching ## Outcome S01 now delivers a job-aware Gmail import loop that is materially closer to the milestone trust bar than the pre-slice state. The workspace can: - connect Gmail and load ranked candidate messages/threads for one owned job - explain why each Gmail candidate matched via score/confidence/match reasons - import either a single message or an entire thread with duplicate-safe result counts - persist Gmail thread identity plus raw sender/recipient labels on correspondence rows - refresh already-linked Gmail threads for that same job and import only later unseen replies - surface that linked-thread state back in the workspace without making the user manually re-import the thread The biggest slice change is that Gmail import is no longer just “pick a message and save a snapshot.” It is now a bounded continuity loop around stored `ExternalThreadId` values. ## What actually shipped ### Backend contract `JobTrackerApi/Controllers/GmailController.cs` and `JobTrackerApi/Services/GmailOAuthService.cs` now cover three distinct Gmail workspace behaviors: 1. **Job-aware candidate ranking** via `GET /api/gmail/job-candidates` - backend aggregates Gmail hits per job query - duplicate Gmail hits are merged server-side - response includes weighted match reasons, matched queries, imported flags, confidence, and thread/message counts 2. **Duplicate-safe import** via `POST /api/gmail/import` and `POST /api/gmail/import-thread` - single-message imports return `Imported`/`Skipped` plus the imported or existing correspondence row - thread imports report imported/skipped counts instead of silently duplicating rows - imported correspondence persists `ExternalMessageId`, `ExternalThreadId`, `ExternalFrom`, and `ExternalTo` 3. **Linked-thread continuity** via `POST /api/gmail/refresh-linked-threads` - reads the owned job’s existing linked Gmail thread ids - fetches those threads from Gmail - skips already-known external message ids - imports only new messages into the same job - returns refresh counts per job and per thread - distinguishes invalid job, disconnected Gmail, empty linked-thread set, and already-current outcomes ### Persistence and compatibility `Models/Correspondence.cs`, `JobTrackerApi/Controllers/CorrespondenceController.cs`, and `JobTrackerApi/Program.cs` now treat Gmail metadata as part of the durable correspondence model, including compatibility guards for the extra columns. ### Workspace UI `job-tracker-ui/src/components/Correspondence.tsx` now makes the job workspace the real Gmail import surface instead of a generic search-first flow. It now: - loads backend-ranked Gmail candidates for the current job - shows confidence/score/match reasons/already-linked status - preserves manual query override through the same endpoint - renders persisted Gmail thread/from/to metadata in the correspondence list - automatically refreshes linked Gmail threads once per loaded job/thread-set - exposes a manual “Refresh linked threads” action for another bounded pull in the same session - updates the workspace after message import, thread import, and linked-thread refresh `job-tracker-ui/src/correspondence-gmail-import.test.tsx` now covers both the ranked import path and the no-manual-reimport continuity path. ## Verification run in this closure session ### Automated checks | Command | Result | |---|---| | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --filter GmailControllerTests` | ✅ passed | | `CI=true npm --prefix job-tracker-ui test -- --watch=false --runTestsByPath src/correspondence-gmail-import.test.tsx` | ✅ passed | | `dotnet build JobTrackerApi/JobTrackerApi.csproj` | ✅ passed | The backend verification command originally described in project knowledge was blocked by unrelated test-project drift earlier in the milestone, but that drift was fixed during this closure pass, and the exact filtered command now passes. ### Observability confirmed The slice’s durable inspection surfaces are now coherent: - `GET /api/gmail/status` exposes Gmail connection freshness (`lastSyncedAt`) - `GET /api/gmail/job-candidates` exposes job-scoped ranking details and duplicate visibility - `POST /api/gmail/refresh-linked-threads` exposes refresh counts and per-thread status - persisted correspondence rows carry external Gmail thread/message/from/to metadata - focused backend/frontend tests encode the intended behavior for future refactors ### Human / live Gmail UAT status This auto-mode session did **not** have a live Gmail account wired for a real-account browser pass, so the required human/live UAT was prepared as a concrete script in `S01-UAT.md` rather than executed here. The implementation and automated slice gates passed; the live-account trust check remains a human runbook item. ## Requirement impact - **R002** moved to **validated** based on the shipped linked-thread refresh contract plus focused backend/frontend verification. - **R010** remains active, but S01 materially advances it by making Gmail correspondence continuity part of the same job history instead of a one-off import snapshot. ## Key downstream implications ### For S02 Imported Gmail correspondence is now trustworthy enough to use as drafting context. Downstream draft generation should consume job-linked correspondence rows directly, including sender/recipient/thread metadata when useful for tone or recipient context. ### For S03 Reply/follow-up drafting can now assume two important invariants: - Gmail correspondence is attached to a specific job - later Gmail replies can be pulled into that same job without user re-import That means reply/follow-up context assembly should build from the persisted job correspondence set, not from transient Gmail candidate data. ### For S04/S05 Daily-loop surfaces can now rely on a clearer distinction between: - jobs that only have candidate Gmail matches - jobs with already-linked live Gmail threads - jobs whose linked threads were refreshed and imported new correspondence Those are useful action/readiness signals for dashboards, follow-up surfaces, and final end-to-end trust checks. ## Notable lessons / non-obvious details - The bounded sync model is deliberate: refresh is over known linked thread ids for one job, not inbox-wide Gmail watch/history state. - The React workspace auto-refresh is intentionally one-shot per `jobId + linked thread set`; repeated pulls in the same session require the explicit refresh action. - The filtered Gmail backend test command compiles the whole `JobTrackerApi.Tests` project before filtering, so unrelated test drift can still block slice verification if future work breaks test signatures again. ## Slice verdict S01 now establishes the milestone’s Gmail foundation: smarter matching, clearer import trust signals, persisted Gmail metadata, and real linked-thread continuity in the job workspace.