171 lines
9.3 KiB
Markdown
171 lines
9.3 KiB
Markdown
---
|
||
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.
|