feat(S05/T02): Added an end-to-end trust-loop regression, surfaced save…

- job-tracker-ui/src/end-to-end-trust-loop.test.tsx
- job-tracker-ui/src/components/JobDetailsDialog.tsx
- job-tracker-ui/src/components/Correspondence.tsx
- .gsd/milestones/M001/slices/S05/S05-UAT.md
This commit is contained in:
2026-03-24 14:36:46 +01:00
parent 9f631ca320
commit a0d1c1c05b
7 changed files with 436 additions and 2 deletions
@@ -405,6 +405,28 @@ export default function Correspondence({ jobId, job }: { jobId: number; job: Job
)}
</Paper>
<Box sx={{ mt: 1.5, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Typography variant="overline">Linked Gmail thread continuity</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.25 }}>
Linked Gmail refresh only checks threads that are already tied to this job, so new correspondence can appear here without re-importing the whole thread.
</Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Chip size="small" color={gmailStatus?.connected ? "success" : "default"} variant="outlined" label={gmailStatus?.connected ? "Gmail connected" : "Gmail not connected"} />
<Chip size="small" color={linkedThreadIds.length > 0 ? "success" : "default"} variant="outlined" label={linkedThreadIds.length > 0 ? `Linked threads: ${linkedThreadIds.length}` : "No linked threads yet"} />
{linkedThreadRefresh ? (
<Chip
size="small"
variant="outlined"
label={linkedThreadRefresh.imported > 0
? `Last linked refresh imported ${linkedThreadRefresh.imported} new message${linkedThreadRefresh.imported === 1 ? "" : "s"}`
: linkedThreadRefresh.hasLinkedThreads
? `Last linked refresh checked ${linkedThreadRefresh.threadsChecked} linked thread${linkedThreadRefresh.threadsChecked === 1 ? "" : "s"}`
: "No linked Gmail refresh history yet"}
/>
) : null}
</Box>
</Box>
<Box sx={{ display: "flex", gap: 1, alignItems: "flex-start", mt: 1.5, flexWrap: "wrap" }}>
<ToggleButtonGroup exclusive value={from} onChange={(_, v) => v && setFrom(v)} size="small">
<ToggleButton value="Me">{t("correspondenceMe")}</ToggleButton>
@@ -553,6 +553,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
<Chip size="small" label={`Cover letter · ${coverLetterStatus.label}`} color={coverLetterStatus.color} />
<Chip size="small" label={`Application answer · ${applicationAnswerStatus.label}`} color={applicationAnswerStatus.color} />
<Chip size="small" label={`Recruiter message · ${recruiterMessageStatus.label}`} color={recruiterMessageStatus.color} />
<Chip size="small" variant="outlined" label="Saved package material feeds follow-up drafting" />
{packageGeneratedAt ? <Chip size="small" variant="outlined" label={`Generated ${new Date(packageGeneratedAt).toLocaleTimeString()}`} /> : null}
</Box>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("jobDetailsTailoredCvIntro")}</Typography>
@@ -584,7 +585,7 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
/>
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Typography variant="overline">Saved working material</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>These saved copies are what later slices can trust and reuse.</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>These saved copies are what follow-up drafting and later slices can trust and reuse.</Typography>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="body2"><strong>Tailored CV:</strong> {(job?.tailoredCvText ?? "").trim() ? "Saved on this job" : "Not saved yet"}</Typography>
<Typography variant="body2"><strong>Cover letter:</strong> {savedPackageWorkspace.coverLetter.trim() ? "Saved on this job" : "Not saved yet"}</Typography>
@@ -632,6 +633,12 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
</FormControl>
<Button variant="outlined" onClick={() => setDraftReloadToken((value) => value + 1)}>{t("jobDetailsRegenerateDraft")}</Button>
</Box>
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "warning.main", backgroundColor: "background.default" }}>
<Typography variant="overline">Manual send boundary</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
Generating or regenerating this grounded draft never sends recruiter email. The only outbound step is the explicit Send and log email action below.
</Typography>
</Box>
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={`${t("jobDetailsRecipientHelp")} Manual send only — nothing is dispatched until you press send.`} />
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} helperText="You can edit this before sending. Sending stays manual and logs the sent note back to correspondence." />