From 9ac28459ed6cd9595370b58df50b52a20945f247 Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:54:31 +0200 Subject: [PATCH] feat(capture): async polling in CaptureDialog + pollCaptureJob api helper --- frontend/src/api.js | 9 +++- frontend/src/components/CaptureDialog.jsx | 52 ++++++++++++++++++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/frontend/src/api.js b/frontend/src/api.js index 1af25c8..d4cbd8e 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -64,9 +64,14 @@ export async function submitCapture(archiveId, locator) { body: JSON.stringify({ locator }), }); if (!res.ok) { - const msg = await res.text(); - throw new Error(msg || `HTTP ${res.status}`); + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `HTTP ${res.status}`); } + return res.json(); // { job_uid, status: "pending" } +} + +export async function pollCaptureJob(archiveId, jobUid) { + return getJson(`/api/archives/${archiveId}/capture_jobs/${jobUid}`); } // ── Auth helpers ───────────────────────────────────────────────────────────── diff --git a/frontend/src/components/CaptureDialog.jsx b/frontend/src/components/CaptureDialog.jsx index 9a6c73c..9942a71 100644 --- a/frontend/src/components/CaptureDialog.jsx +++ b/frontend/src/components/CaptureDialog.jsx @@ -1,16 +1,21 @@ import { useRef, useEffect, useState } from 'react' -import { submitCapture } from '../api' +import { submitCapture, pollCaptureJob } from '../api' export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) { const dialogRef = useRef(null) const [locator, setLocator] = useState('') const [error, setError] = useState(null) const [busy, setBusy] = useState(false) + const [jobStatus, setJobStatus] = useState(null) // null | 'running' | 'completed' | 'failed' + const pollRef = useRef(null) useEffect(() => { const dialog = dialogRef.current if (!dialog) return - const handleClose = () => onClose() + const handleClose = () => { + clearInterval(pollRef.current) + onClose() + } dialog.addEventListener('close', handleClose) return () => dialog.removeEventListener('close', handleClose) }, [onClose]) @@ -21,6 +26,9 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) if (open) { setLocator('') setError(null) + setJobStatus(null) + setBusy(false) + clearInterval(pollRef.current) if (!dialog.open) dialog.showModal() } else { if (dialog.open) dialog.close() @@ -31,17 +39,47 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) if (!locator.trim()) { setError('Enter a locator.'); return } setBusy(true) setError(null) + setJobStatus(null) try { - await submitCapture(archiveId, locator.trim()) - dialogRef.current?.close() - onCaptured() + const job = await submitCapture(archiveId, locator.trim()) + setJobStatus('running') + pollRef.current = setInterval(async () => { + try { + const updated = await pollCaptureJob(archiveId, job.job_uid) + if (updated.status === 'completed') { + clearInterval(pollRef.current) + pollRef.current = null + setBusy(false) + setJobStatus('completed') + dialogRef.current?.close() + onCaptured() + } else if (updated.status === 'failed') { + clearInterval(pollRef.current) + pollRef.current = null + setBusy(false) + setJobStatus('failed') + setError(updated.error_text || 'Capture failed.') + } + // pending / running: keep polling + } catch (pollErr) { + clearInterval(pollRef.current) + pollRef.current = null + setBusy(false) + setError(pollErr.message) + } + }, 500) } catch (e) { setError(e.message) - } finally { setBusy(false) } } + function buttonLabel() { + if (!busy) return 'Capture' + if (jobStatus === 'running') return 'Running\u2026' + return 'Capturing\u2026' + } + return (
@@ -56,7 +94,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })