fix(ocr): resume polling on page reload + track single-doc job status
Single-document OCR now creates an OcrJobDocument row so
GET /api/documents/{id}/ocr-status can find running jobs.
OcrAsyncRunner updates the job document status (RUNNING → DONE/FAILED).
Frontend checks OCR status when entering transcription mode —
if a job is running, resumes polling and shows the spinner.
Refs #226
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,16 +39,32 @@ public class OcrAsyncRunner {
|
|||||||
job.setStatus(OcrJobStatus.RUNNING);
|
job.setStatus(OcrJobStatus.RUNNING);
|
||||||
ocrJobRepository.save(job);
|
ocrJobRepository.save(job);
|
||||||
|
|
||||||
|
OcrJobDocument jobDoc = ocrJobDocumentRepository.findByJobIdAndDocumentId(jobId, documentId)
|
||||||
|
.orElse(null);
|
||||||
|
if (jobDoc != null) {
|
||||||
|
jobDoc.setStatus(OcrDocumentStatus.RUNNING);
|
||||||
|
ocrJobDocumentRepository.save(jobDoc);
|
||||||
|
}
|
||||||
|
|
||||||
Document doc = documentService.getDocumentById(documentId);
|
Document doc = documentService.getDocumentById(documentId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processDocument(documentId, doc, userId);
|
processDocument(documentId, doc, userId);
|
||||||
job.setStatus(OcrJobStatus.DONE);
|
job.setStatus(OcrJobStatus.DONE);
|
||||||
job.setProcessedDocuments(1);
|
job.setProcessedDocuments(1);
|
||||||
|
if (jobDoc != null) {
|
||||||
|
jobDoc.setStatus(OcrDocumentStatus.DONE);
|
||||||
|
ocrJobDocumentRepository.save(jobDoc);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("OCR processing failed for document {}", documentId, e);
|
log.error("OCR processing failed for document {}", documentId, e);
|
||||||
job.setStatus(OcrJobStatus.FAILED);
|
job.setStatus(OcrJobStatus.FAILED);
|
||||||
job.setErrorCount(1);
|
job.setErrorCount(1);
|
||||||
|
if (jobDoc != null) {
|
||||||
|
jobDoc.setStatus(OcrDocumentStatus.FAILED);
|
||||||
|
jobDoc.setErrorMessage(e.getMessage());
|
||||||
|
ocrJobDocumentRepository.save(jobDoc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ocrJobRepository.save(job);
|
ocrJobRepository.save(job);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.*;
|
import org.raddatz.familienarchiv.model.*;
|
||||||
|
import org.raddatz.familienarchiv.repository.OcrJobDocumentRepository;
|
||||||
import org.raddatz.familienarchiv.repository.OcrJobRepository;
|
import org.raddatz.familienarchiv.repository.OcrJobRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ public class OcrService {
|
|||||||
private final OcrHealthClient ocrHealthClient;
|
private final OcrHealthClient ocrHealthClient;
|
||||||
private final DocumentService documentService;
|
private final DocumentService documentService;
|
||||||
private final OcrJobRepository ocrJobRepository;
|
private final OcrJobRepository ocrJobRepository;
|
||||||
|
private final OcrJobDocumentRepository ocrJobDocumentRepository;
|
||||||
private final OcrAsyncRunner ocrAsyncRunner;
|
private final OcrAsyncRunner ocrAsyncRunner;
|
||||||
|
|
||||||
public UUID startOcr(UUID documentId, ScriptType scriptTypeOverride, UUID userId) {
|
public UUID startOcr(UUID documentId, ScriptType scriptTypeOverride, UUID userId) {
|
||||||
@@ -44,6 +46,13 @@ public class OcrService {
|
|||||||
.build();
|
.build();
|
||||||
job = ocrJobRepository.save(job);
|
job = ocrJobRepository.save(job);
|
||||||
|
|
||||||
|
OcrJobDocument jobDoc = OcrJobDocument.builder()
|
||||||
|
.jobId(job.getId())
|
||||||
|
.documentId(documentId)
|
||||||
|
.status(OcrDocumentStatus.PENDING)
|
||||||
|
.build();
|
||||||
|
ocrJobDocumentRepository.save(jobDoc);
|
||||||
|
|
||||||
ocrAsyncRunner.runSingleDocument(job.getId(), documentId, userId);
|
ocrAsyncRunner.runSingleDocument(job.getId(), documentId, userId);
|
||||||
return job.getId();
|
return job.getId();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.*;
|
import org.raddatz.familienarchiv.model.*;
|
||||||
|
import org.raddatz.familienarchiv.repository.OcrJobDocumentRepository;
|
||||||
import org.raddatz.familienarchiv.repository.OcrJobRepository;
|
import org.raddatz.familienarchiv.repository.OcrJobRepository;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,6 +25,7 @@ class OcrServiceTest {
|
|||||||
@Mock OcrHealthClient ocrHealthClient;
|
@Mock OcrHealthClient ocrHealthClient;
|
||||||
@Mock DocumentService documentService;
|
@Mock DocumentService documentService;
|
||||||
@Mock OcrJobRepository ocrJobRepository;
|
@Mock OcrJobRepository ocrJobRepository;
|
||||||
|
@Mock OcrJobDocumentRepository ocrJobDocumentRepository;
|
||||||
@Mock OcrAsyncRunner ocrAsyncRunner;
|
@Mock OcrAsyncRunner ocrAsyncRunner;
|
||||||
|
|
||||||
@InjectMocks OcrService ocrService;
|
@InjectMocks OcrService ocrService;
|
||||||
|
|||||||
@@ -251,12 +251,28 @@ function handleParagraphClick(annotationId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load blocks when transcribe mode is entered and set default panel mode
|
async function checkOcrStatus() {
|
||||||
|
if (!doc?.id) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/documents/${doc.id}/ocr-status`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const status = await res.json();
|
||||||
|
if ((status.status === 'PENDING' || status.status === 'RUNNING') && status.jobId) {
|
||||||
|
ocrRunning = true;
|
||||||
|
pollOcrJob(status.jobId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load blocks and check OCR status when transcribe mode is entered
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (transcribeMode) {
|
if (transcribeMode) {
|
||||||
loadTranscriptionBlocks().then(() => {
|
loadTranscriptionBlocks().then(() => {
|
||||||
panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit';
|
panelMode = transcriptionBlocks.length > 0 ? 'read' : 'edit';
|
||||||
});
|
});
|
||||||
|
checkOcrStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user