Skip to content

Commit e48472c

Browse files
committed
fix: narrow provenance 404 catch, revert global log downgrade, gate empty-state on loaded id
- cardsApi.getCardProvenance: only return null for 404s whose message starts with "Capture provenance not found"; rethrow card-not-found 404s so they surface as real errors instead of silently showing manual-card empty state (addresses Copilot review comment) - http.ts interceptor: revert global 404 warn downgrade — the API layer now handles the expected absence silently so the interceptor no longer needs special-casing, and downgrading all 404s globally reduces observability for genuine missing-resource errors (addresses Gemini review comment) - CardModal.vue: gate provenance empty-state on loadedCaptureProvenanceCardId === card.id so the "Created manually" message does not flash for capture-pipeline cards while comments are being awaited before provenance fetch starts (addresses Copilot review comment) - cardsApi.spec.ts: update 404 test to use the narrowed message check, add test for card-not-found 404 that must rethrow (1547 tests pass)
1 parent ac82418 commit e48472c

File tree

4 files changed

+30
-14
lines changed

4 files changed

+30
-14
lines changed

frontend/taskdeck-web/src/api/cardsApi.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ export const cardsApi = {
3636
const { data } = await http.get<CardCaptureProvenance>(`/boards/${boardId}/cards/${cardId}/provenance`)
3737
return data
3838
} catch (e: unknown) {
39-
const candidate = e as { response?: { status?: number } } | null
40-
if (candidate?.response?.status === 404) {
41-
// Manual cards have no capture provenance — treat absence as empty state, not an error.
39+
const candidate = e as { response?: { status?: number; data?: { message?: string } } } | null
40+
if (
41+
candidate?.response?.status === 404 &&
42+
typeof candidate.response.data?.message === 'string' &&
43+
candidate.response.data.message.startsWith('Capture provenance not found')
44+
) {
45+
// Manual cards have no capture provenance — treat only that specific absence as
46+
// empty state, not an error. Other 404s (e.g. card not found in board) are rethrown
47+
// so callers can surface them as genuine errors.
4248
return null
4349
}
4450
throw e

frontend/taskdeck-web/src/api/http.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,7 @@ http.interceptors.response.use(
4545
(response) => response,
4646
(error) => {
4747
if (error.response) {
48-
// 404 is an expected "not found" state for optional resources (e.g. capture
49-
// provenance on manually-created cards). Log at warn level so callers that
50-
// handle the absence silently don't pollute the console with error noise.
51-
if (error.response.status === 404) {
52-
console.warn('API Not Found:', error.response.data)
53-
} else {
54-
console.error('API Error:', error.response.data)
55-
}
48+
console.error('API Error:', error.response.data)
5649

5750
// Handle 401 - clear session and redirect to login (skip in demo mode)
5851
if (error.response.status === 401 && !isDemoMode) {

frontend/taskdeck-web/src/components/board/CardModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ onBeforeUnmount(() => {
608608
Triage run: {{ captureProvenance.triageRunId }}
609609
</p>
610610
</div>
611-
<p v-else class="text-xs text-on-surface-variant italic" data-testid="provenance-empty-state">Created manually — no capture provenance.</p>
611+
<p v-else-if="loadedCaptureProvenanceCardId === card.id" class="text-xs text-on-surface-variant italic" data-testid="provenance-empty-state">Created manually — no capture provenance.</p>
612612
</div>
613613
</div>
614614
</div>

frontend/taskdeck-web/src/tests/api/cardsApi.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,32 @@ describe('cardsApi', () => {
136136
expect(result).toEqual(provenance)
137137
})
138138

139-
it('should return null for manually-created cards with no capture provenance (404)', async () => {
140-
const notFoundError = { response: { status: 404, data: { errorCode: 'NotFound' } } }
139+
it('should return null for manually-created cards with no capture provenance (404 + provenance message)', async () => {
140+
const notFoundError = {
141+
response: {
142+
status: 404,
143+
data: { errorCode: 'NotFound', message: 'Capture provenance not found for card manual-card-1' },
144+
},
145+
}
141146
vi.mocked(http.get).mockRejectedValue(notFoundError)
142147

143148
const result = await cardsApi.getCardProvenance('board-1', 'manual-card-1')
144149

145150
expect(result).toBeNull()
146151
})
147152

153+
it('should rethrow 404 when the card itself is not found (card-not-found, not provenance-absent)', async () => {
154+
const cardNotFoundError = {
155+
response: {
156+
status: 404,
157+
data: { errorCode: 'NotFound', message: 'Card with ID stale-id not found in board board-1' },
158+
},
159+
}
160+
vi.mocked(http.get).mockRejectedValue(cardNotFoundError)
161+
162+
await expect(cardsApi.getCardProvenance('board-1', 'stale-id')).rejects.toEqual(cardNotFoundError)
163+
})
164+
148165
it('should rethrow non-404 errors from the provenance endpoint', async () => {
149166
const serverError = { response: { status: 500, data: { errorCode: 'InternalError' } } }
150167
vi.mocked(http.get).mockRejectedValue(serverError)

0 commit comments

Comments
 (0)