Files

171 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 jobs 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 slices 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 milestones Gmail foundation: smarter matching, clearer import trust signals, persisted Gmail metadata, and real linked-thread continuity in the job workspace.