1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(capture): async polling in CaptureDialog + pollCaptureJob api helper

This commit is contained in:
TheGeneralist 2026-06-26 12:54:31 +02:00
parent f76f5438f2
commit 9ac28459ed
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
2 changed files with 52 additions and 9 deletions

View file

@ -64,9 +64,14 @@ export async function submitCapture(archiveId, locator) {
body: JSON.stringify({ locator }), body: JSON.stringify({ locator }),
}); });
if (!res.ok) { if (!res.ok) {
const msg = await res.text(); const body = await res.json().catch(() => ({}));
throw new Error(msg || `HTTP ${res.status}`); 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 ───────────────────────────────────────────────────────────── // ── Auth helpers ─────────────────────────────────────────────────────────────

View file

@ -1,16 +1,21 @@
import { useRef, useEffect, useState } from 'react' import { useRef, useEffect, useState } from 'react'
import { submitCapture } from '../api' import { submitCapture, pollCaptureJob } from '../api'
export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) { export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) {
const dialogRef = useRef(null) const dialogRef = useRef(null)
const [locator, setLocator] = useState('') const [locator, setLocator] = useState('')
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
const [jobStatus, setJobStatus] = useState(null) // null | 'running' | 'completed' | 'failed'
const pollRef = useRef(null)
useEffect(() => { useEffect(() => {
const dialog = dialogRef.current const dialog = dialogRef.current
if (!dialog) return if (!dialog) return
const handleClose = () => onClose() const handleClose = () => {
clearInterval(pollRef.current)
onClose()
}
dialog.addEventListener('close', handleClose) dialog.addEventListener('close', handleClose)
return () => dialog.removeEventListener('close', handleClose) return () => dialog.removeEventListener('close', handleClose)
}, [onClose]) }, [onClose])
@ -21,6 +26,9 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
if (open) { if (open) {
setLocator('') setLocator('')
setError(null) setError(null)
setJobStatus(null)
setBusy(false)
clearInterval(pollRef.current)
if (!dialog.open) dialog.showModal() if (!dialog.open) dialog.showModal()
} else { } else {
if (dialog.open) dialog.close() 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 } if (!locator.trim()) { setError('Enter a locator.'); return }
setBusy(true) setBusy(true)
setError(null) setError(null)
setJobStatus(null)
try { try {
await submitCapture(archiveId, locator.trim()) const job = await submitCapture(archiveId, locator.trim())
dialogRef.current?.close() setJobStatus('running')
onCaptured() 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) { } catch (e) {
setError(e.message) setError(e.message)
} finally {
setBusy(false) setBusy(false)
} }
} }
function buttonLabel() {
if (!busy) return 'Capture'
if (jobStatus === 'running') return 'Running\u2026'
return 'Capturing\u2026'
}
return ( return (
<dialog ref={dialogRef} className="capture-dialog"> <dialog ref={dialogRef} className="capture-dialog">
<div className="capture-dialog-inner"> <div className="capture-dialog-inner">
@ -56,7 +94,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
<div className="capture-actions"> <div className="capture-actions">
<button type="button" className="capture-cancel" onClick={() => dialogRef.current?.close()}>Cancel</button> <button type="button" className="capture-cancel" onClick={() => dialogRef.current?.close()}>Cancel</button>
<button type="button" className="capture-submit" onClick={handleSubmit} disabled={busy}> <button type="button" className="capture-submit" onClick={handleSubmit} disabled={busy}>
{busy ? 'Capturing\u2026' : 'Capture'} {buttonLabel()}
</button> </button>
</div> </div>
</div> </div>