diff --git a/src/composables/__tests__/useDashboardSuggestions.spec.ts b/src/composables/__tests__/useDashboardSuggestions.spec.ts index ab46decb..5e273f81 100644 --- a/src/composables/__tests__/useDashboardSuggestions.spec.ts +++ b/src/composables/__tests__/useDashboardSuggestions.spec.ts @@ -76,6 +76,43 @@ describe('useSuggestionRunPoller', () => { scope.stop(); }); + it('surfaces failure details when a completed run reports failed cells', async () => { + mocks.execute.mockResolvedValueOnce({ + data: { + value: { + data: { + id: 'run-1', + status: 'completed', + plannedCalls: 2, + completedCells: 1, + failedCells: 1, + stats: { + failedCells: [{ cellIndex: 2, error: 'model timed out' }], + }, + }, + }, + }, + }); + + const scope = effectScope(); + const poller = scope.run(() => useSuggestionRunPoller(ref('ssp-1'))); + + await poller?.pollLatest(); + + expect(mocks.toastAdd).toHaveBeenCalledWith( + expect.objectContaining({ + severity: 'warn', + summary: '1 suggestion failed', + detail: 'Cell 2: model timed out', + }), + ); + expect(mocks.toastAdd).not.toHaveBeenCalledWith( + expect.objectContaining({ summary: 'Suggestions ready' }), + ); + + scope.stop(); + }); + it('keeps polling after a transient poll error', async () => { mocks.execute .mockRejectedValueOnce(new Error('temporary outage')) diff --git a/src/composables/useDashboardSuggestions.ts b/src/composables/useDashboardSuggestions.ts index 42e0e191..8fbe9231 100644 --- a/src/composables/useDashboardSuggestions.ts +++ b/src/composables/useDashboardSuggestions.ts @@ -25,6 +25,7 @@ import { type GenerateDashboardSuggestionsPayload, type SuggestionRun, isRunActive, + runCellFailures, } from '@/views/dashboard/partials/dashboard-suggestions'; export function useDashboardSuggestions( @@ -72,7 +73,11 @@ export function useDashboardSuggestions( return fetchPendingSuggestions( buildDashboardSuggestionsEndpoint(sspId.value, 'pending'), - { camelcaseStopPaths: ['data.labelSet'] }, + // Preserve raw label keys (e.g. `_policy`) on both the originating label + // set and the proposed filter; otherwise camelcase conversion strips `_`. + { + camelcaseStopPaths: ['data.labelSet', 'data.proposedFilterLabelSet'], + }, ); } @@ -132,7 +137,9 @@ export function useDashboardSuggestions( for (const status of statuses) { const response = await fetchHistoryRequest( buildDashboardSuggestionsEndpoint(sspId.value, status), - { camelcaseStopPaths: ['data.labelSet'] }, + { + camelcaseStopPaths: ['data.labelSet', 'data.proposedFilterLabelSet'], + }, ); collected.push(...(response?.data.value?.data ?? [])); } @@ -192,6 +199,28 @@ export function useDashboardSuggestions( }; } +// Builds a human-readable detail string for a run that reported failures, +// preferring per-cell error messages (from `stats.failedCells`) over the +// generic top-level error. +function formatRunFailureDetail(run: SuggestionRun): string { + const failures = runCellFailures(run); + if (failures.length === 0) { + return run.error ?? 'Dashboard suggestion generation failed.'; + } + + const shown = failures.slice(0, 3).map((failure) => { + const label = + failure.cellIndex === undefined ? 'Cell' : `Cell ${failure.cellIndex}`; + return `${label}: ${failure.error ?? 'Failed'}`; + }); + + const remaining = failures.length - shown.length; + if (remaining > 0) { + shown.push(`and ${remaining} more`); + } + return shown.join('; '); +} + interface SuggestionRunPollerOptions { stopOnPollError?: boolean; } @@ -257,20 +286,26 @@ export function useSuggestionRunPoller( } terminalToastShownFor.value = runKey; - if (latestRun.status === 'completed') { + const hasFailures = + latestRun.status === 'failed' || (latestRun.failedCells ?? 0) > 0; + + if (hasFailures) { + toast.add({ + severity: latestRun.status === 'failed' ? 'error' : 'warn', + summary: + latestRun.status === 'failed' + ? 'Generation failed' + : `${latestRun.failedCells} suggestion${latestRun.failedCells === 1 ? '' : 's'} failed`, + detail: formatRunFailureDetail(latestRun), + life: 8000, + }); + } else if (latestRun.status === 'completed') { toast.add({ severity: 'success', summary: 'Suggestions ready', detail: `${latestRun.completedCells} cells completed`, life: 3000, }); - } else if (latestRun.status === 'failed') { - toast.add({ - severity: 'error', - summary: 'Generation failed', - detail: latestRun.error ?? 'Dashboard suggestion generation failed.', - life: 5000, - }); } } catch { if (options.stopOnPollError) { diff --git a/src/views/dashboard/SuggestionsView.vue b/src/views/dashboard/SuggestionsView.vue index ae64b035..2e4ccc5a 100644 --- a/src/views/dashboard/SuggestionsView.vue +++ b/src/views/dashboard/SuggestionsView.vue @@ -61,16 +61,24 @@ -

{{ run.error ?? 'Suggestion generation failed.' }}

-