MVP frontend+backend

This commit is contained in:
2026-06-14 09:58:30 +02:00
parent d9539f23cc
commit 772d853e37
56 changed files with 5099 additions and 207 deletions
+132
View File
@@ -0,0 +1,132 @@
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'
interface PollResponse {
status: ScraperStatus
qr_data?: string
result?: Record<string, unknown>
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<void>
cancel: () => Promise<void>
isRunning: boolean
isTerminal: boolean
}
export function useScraperFetch(): UseScraperFetchResult {
const [sessionId, setSessionId] = useState<string | null>(null)
const [pollData, setPollData] = useState<PollResponse | null>(null)
const [resolvedIds, setResolvedIds] = useState<ResolvedIds | null>(null)
const [error, setError] = useState<string | null>(null)
const [unauthorizedError, setUnauthorizedError] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | 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,
}
}