interacmanagernew / packages /web /src /hooks /useEmailScan.ts
Heaven K
fix: stop infinite 404 polling on stale scan job IDs
3ed3f80
import { useState, useCallback, useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api } from '../services/api';
import type { ScanPreset } from '@icc/shared';
interface ScanStartResponse {
jobId: string;
dateRange: any;
status: string;
}
interface ScanStatusResponse {
jobId: string;
status: string;
dateRange: any;
progress: {
emailsFound: number;
emailsProcessed: number;
emailsSkipped: number;
emailsErrored: number;
currentEmail?: string;
};
startedAt: string;
completedAt?: string;
}
export function useEmailScan() {
const queryClient = useQueryClient();
const [activeJobId, setActiveJobId] = useState<string | null>(null);
const startScanMutation = useMutation({
mutationFn: (params: {
preset: ScanPreset;
startDate?: string;
endDate?: string;
forceRescan?: boolean;
}) => api.post<ScanStartResponse>('/scan/start', params),
onSuccess: (data) => {
setActiveJobId(data.jobId);
},
});
const scanStatusQuery = useQuery<ScanStatusResponse>({
queryKey: ['scanStatus', activeJobId],
queryFn: () => api.get<ScanStatusResponse>(`/scan/status/${activeJobId}`),
enabled: !!activeJobId,
retry: (failureCount, error: any) => {
// Stop retrying on 404 (stale job ID after server restart)
if (error?.status === 404 || error?.response?.status === 404) return false;
return failureCount < 3;
},
refetchInterval: (query) => {
const data = query.state.data;
if (data?.status === 'completed' || data?.status === 'failed') {
return false; // Stop polling
}
// Stop polling on error (404, network error, etc.)
if (query.state.status === 'error') {
return false;
}
return 1000; // Poll every 1s while scanning
},
});
const startScan = useCallback((
preset: ScanPreset,
options?: { startDate?: string; endDate?: string; forceRescan?: boolean }
) => {
startScanMutation.mutate({ preset, ...options });
}, [startScanMutation]);
// Clear stale job ID on 404 (server restarted, job no longer exists)
useEffect(() => {
if (activeJobId && scanStatusQuery.isError) {
setActiveJobId(null);
}
}, [activeJobId, scanStatusQuery.isError]);
const isScanning = scanStatusQuery.data?.status === 'scanning' ||
scanStatusQuery.data?.status === 'parsing' ||
scanStatusQuery.data?.status === 'queued';
const clearJob = useCallback(() => {
setActiveJobId(null);
queryClient.invalidateQueries({ queryKey: ['transactions'] });
queryClient.invalidateQueries({ queryKey: ['transactionStats'] });
}, [queryClient]);
return {
startScan,
isScanning,
activeJobId,
scanStatus: scanStatusQuery.data ?? null,
scanError: startScanMutation.error,
isStarting: startScanMutation.isPending,
clearJob,
};
}
export function useScanHistory() {
return useQuery({
queryKey: ['scanHistory'],
queryFn: () => api.get<{ scans: any[] }>('/scan/history'),
});
}