fix: contacts fetch error degrades partially, not full panel blank

Contacts 5xx no longer early-returns from fetchFor, leaving the entire
right panel blank. A new contactsError ref surfaces the failure message
in the Email tab only; JD tab, Cover Letter tab, and match score all
render normally. Adds test asserting partial degradation behavior.
This commit is contained in:
pyr0ball 2026-03-20 19:16:03 -07:00
parent 8479f79701
commit b1a32ab207
3 changed files with 58 additions and 6 deletions

View file

@ -151,6 +151,36 @@ describe('usePrepStore', () => {
expect(store.research).toBeNull()
expect(store.contacts).toEqual([])
expect(store.contactsError).toBeNull()
expect(store.currentJobId).toBeNull()
})
it('fetchFor sets contactsError and leaves other data intact when contacts fetch fails', async () => {
const mockApiFetch = vi.mocked(useApiFetch)
mockApiFetch
.mockResolvedValueOnce({ data: { company_brief: 'Acme', ceo_brief: null, talking_points: null,
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
generated_at: '2026-03-20T12:00:00' }, error: null }) // research OK
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 500, detail: 'DB error' } }) // contacts fail
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task OK
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
description: 'Build things.', cover_letter: null, match_score: 80,
keyword_gaps: null }, error: null }) // fullJob OK
const store = usePrepStore()
await store.fetchFor(1)
// Contacts error shown in Email tab only
expect(store.contactsError).toBe('Could not load email history.')
expect(store.contacts).toEqual([])
// Everything else still renders
expect(store.research?.company_brief).toBe('Acme')
expect(store.fullJob?.description).toBe('Build things.')
expect(store.fullJob?.match_score).toBe(80)
expect(store.taskStatus.status).toBe('none')
// Top-level error stays null (no full-panel blank-out)
expect(store.error).toBeNull()
})
})

View file

@ -42,6 +42,7 @@ export interface FullJobDetail {
export const usePrepStore = defineStore('prep', () => {
const research = ref<ResearchBrief | null>(null)
const contacts = ref<Contact[]>([])
const contactsError = ref<string | null>(null)
const taskStatus = ref<TaskStatus>({ status: null, stage: null, message: null })
const fullJob = ref<FullJobDetail | null>(null)
const loading = ref(false)
@ -62,6 +63,7 @@ export const usePrepStore = defineStore('prep', () => {
_clearInterval()
research.value = null
contacts.value = []
contactsError.value = null
taskStatus.value = { status: null, stage: null, message: null }
fullJob.value = null
error.value = null
@ -82,17 +84,22 @@ export const usePrepStore = defineStore('prep', () => {
error.value = 'Failed to load research data'
return
}
if (contactsResult.error) {
error.value = 'Failed to load contacts'
return
}
if (jobResult.error) {
error.value = 'Failed to load job details'
return
}
research.value = researchResult.data ?? null
contacts.value = contactsResult.data ?? []
// Contacts failure is non-fatal — degrade the Email tab only
if (contactsResult.error) {
contactsError.value = 'Could not load email history.'
contacts.value = []
} else {
contacts.value = contactsResult.data ?? []
contactsError.value = null
}
taskStatus.value = taskResult.data ?? { status: null, stage: null, message: null }
fullJob.value = jobResult.data ?? null
@ -141,6 +148,7 @@ export const usePrepStore = defineStore('prep', () => {
_clearInterval()
research.value = null
contacts.value = []
contactsError.value = null
taskStatus.value = { status: null, stage: null, message: null }
fullJob.value = null
loading.value = false
@ -151,6 +159,7 @@ export const usePrepStore = defineStore('prep', () => {
return {
research,
contacts,
contactsError,
taskStatus,
fullJob,
loading,

View file

@ -395,7 +395,10 @@ async function onGenerate() {
role="tabpanel"
aria-labelledby="tab-email"
>
<template v-if="prepStore.contacts.length">
<div v-if="prepStore.contactsError" class="error-state" role="alert">
{{ prepStore.contactsError }}
</div>
<template v-else-if="prepStore.contacts.length">
<div
v-for="contact in prepStore.contacts"
:key="contact.id"
@ -710,6 +713,16 @@ async function onGenerate() {
font-size: var(--text-sm);
}
/* Inline error state for tab panels (e.g. contacts fetch failure) */
.error-state {
background: color-mix(in srgb, var(--color-error) 8%, var(--color-surface));
color: var(--color-error);
border: 1px solid color-mix(in srgb, var(--color-error) 25%, transparent);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
}
/* ── Research sections ───────────────────────────────────────────────────── */
.research-sections {
display: flex;