import { useState, useRef, useCallback } from 'react' import * as api from '@/services/api' import { isUnauthorized } from '@/services/errors' export type ScraperStatus = 'idle' | 'launching' | 'awaiting_scan' | 'authenticating' | 'fetching' | 'done' | 'error' | 'cancelled' // Exported so test fixtures (frontend/tests/fixtures/scraper.ts) can build a // fully-typed poll response instead of casting a partial object to `any`. export interface PollResponse { status: ScraperStatus qr_data?: string result?: Record message?: string } interface ResolvedIds { workspace_id: string personal_data_uuid: string child_id: string } const POLL_INTERVAL_MS = 1000 interface UseScraperFetchResult { sessionId: string | null pollData: PollResponse | null resolvedIds: ResolvedIds | null error: string | null isUnauthorized: boolean start: (childId?: string) => Promise cancel: () => Promise isRunning: boolean isTerminal: boolean } export function useScraperFetch(): UseScraperFetchResult { const [sessionId, setSessionId] = useState(null) const [pollData, setPollData] = useState(null) const [resolvedIds, setResolvedIds] = useState(null) const [error, setError] = useState(null) const [unauthorizedError, setUnauthorizedError] = useState(false) const intervalRef = useRef | null>(null) const stopPolling = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current) intervalRef.current = null } }, []) const isTerminalStatus = (s: ScraperStatus) => s === 'done' || s === 'error' || s === 'cancelled' const startPolling = useCallback((sid: string) => { stopPolling() intervalRef.current = setInterval(async () => { try { const data = await api.pollScraperSession(sid) setPollData(data as PollResponse) if (isTerminalStatus((data as PollResponse).status)) { stopPolling() } } catch (err) { setError(err instanceof Error ? err.message : 'Poll error') stopPolling() } }, POLL_INTERVAL_MS) }, [stopPolling]) const start = useCallback( async (childId?: string) => { if (sessionId && pollData && !isTerminalStatus(pollData.status)) { stopPolling() try { await api.cancelScraperSession(sessionId) } catch { /* ignore */ } } setError(null) setUnauthorizedError(false) setPollData(null) setSessionId(null) setResolvedIds(null) try { const payload = childId ? { child_id: childId } : {} const resp = await api.startScraperSession(payload) setSessionId(resp.session_id) setResolvedIds({ workspace_id: resp.workspace_id, personal_data_uuid: resp.personal_data_uuid, child_id: resp.child_id, }) setPollData({ status: 'launching' }) startPolling(resp.session_id) } catch (err) { if (isUnauthorized(err)) { setUnauthorizedError(true) setError('Not authenticated') } else { setError(err instanceof Error ? err.message : 'Start error') } } }, [sessionId, pollData, stopPolling, startPolling], ) const cancel = useCallback(async () => { stopPolling() if (sessionId) { try { await api.cancelScraperSession(sessionId) } catch { /* ignore */ } } setPollData({ status: 'cancelled' }) setSessionId(null) }, [sessionId, stopPolling]) const isRunning = sessionId !== null && pollData !== null && !isTerminalStatus(pollData.status) const isTerminal = pollData !== null && isTerminalStatus(pollData.status) return { sessionId, pollData, resolvedIds, error, isUnauthorized: unauthorizedError, start, cancel, isRunning, isTerminal, } }