diff --git a/dashboard/src/components/document-sandbox.tsx b/dashboard/src/components/document-sandbox.tsx index d06df4eb..d69608f9 100644 --- a/dashboard/src/components/document-sandbox.tsx +++ b/dashboard/src/components/document-sandbox.tsx @@ -15,6 +15,7 @@ import { isDocumentJobActive, DocumentUploadError, parseDocumentSandboxSession, + resolveDocumentSandboxSessionRestore, SANDBOX_FORBIDDEN_MESSAGE, serializeDocumentSandboxSession, type DocumentJobResult, @@ -79,7 +80,14 @@ export function DocumentSandbox() { } useEffect(() => { - const session = parseDocumentSandboxSession(window.sessionStorage.getItem(SANDBOX_SESSION_STORAGE_KEY)); + let session: ReturnType; + try { + session = parseDocumentSandboxSession(window.sessionStorage.getItem(SANDBOX_SESSION_STORAGE_KEY)); + } catch { + setMessage("This browser cannot access the previous sandbox OCR session."); + setRestoring(false); + return; + } if (!session) { setRestoring(false); return; @@ -89,24 +97,35 @@ export function DocumentSandbox() { setMessage("Restoring the last sandbox OCR session…"); void (async () => { try { - const [restoredUpload, restoredJob] = await Promise.all([ + const [uploadResult, jobResult] = await Promise.allSettled([ session.upload_id ? getSandboxDocumentStatus(session.upload_id) : Promise.resolve(null), session.job_id ? getSandboxDocumentJobStatus(session.job_id) : Promise.resolve(null), ]); if (cancelled) return; + const restored = resolveDocumentSandboxSessionRestore(uploadResult, jobResult); + const { upload: restoredUpload, job: restoredJob } = restored; if (restoredUpload) setUpload(restoredUpload); + if (restored.uploadUnavailable) recordActivity("The previous sandbox upload is no longer available."); if (restoredJob) { setJob(restoredJob); recordActivity(`OCR job status restored: ${restoredJob.status}.`); if (RESULT_READY_JOB_STATUSES.has(restoredJob.status)) { try { - setResult(await getSandboxDocumentJobResult(restoredJob.job_id)); + const restoredResult = await getSandboxDocumentJobResult(restoredJob.job_id); + if (!cancelled) setResult(restoredResult); } catch (error) { if (!(error instanceof DocumentResultNotReadyError)) throw error; } } } - if (!cancelled) setMessage("The last sandbox OCR session was restored."); + if (restored.jobUnavailable) recordActivity("The previous sandbox OCR job is no longer available."); + if (!cancelled) { + setMessage( + restoredUpload || restoredJob + ? "The last sandbox OCR session was restored." + : "The previous sandbox OCR session is no longer available.", + ); + } } catch (error) { if (!cancelled) { setMessage(describe(error, "The previous sandbox OCR session could not be restored. Try refreshing again shortly.")); diff --git a/dashboard/src/lib/document-intelligence.test.ts b/dashboard/src/lib/document-intelligence.test.ts index 3e288fea..ead33c6f 100644 --- a/dashboard/src/lib/document-intelligence.test.ts +++ b/dashboard/src/lib/document-intelligence.test.ts @@ -11,6 +11,7 @@ import { parseDocumentJobState, parseDocumentSandboxSession, parseDocumentUploadState, + resolveDocumentSandboxSessionRestore, } from "./document-intelligence.ts"; test("restores only opaque sandbox identifiers from a browser session", () => { @@ -26,6 +27,21 @@ test("rejects browser session data that includes document content or invalid ide assert.equal(parseDocumentSandboxSession("not json"), null); }); +test("keeps an authorized OCR job when its upload record can no longer be restored", () => { + assert.deepEqual( + resolveDocumentSandboxSessionRestore( + { status: "rejected", reason: new Error("upload expired") }, + { status: "fulfilled", value: { job_id: "job_01TEST", status: "completed" } }, + ), + { + upload: null, + job: { job_id: "job_01TEST", status: "completed" }, + uploadUnavailable: true, + jobUnavailable: false, + }, + ); +}); + test("allows cancellation only while an OCR job is non-terminal", () => { assert.equal(canCancelDocumentJob("accepted"), true); assert.equal(canCancelDocumentJob("processing"), true); diff --git a/dashboard/src/lib/document-intelligence.ts b/dashboard/src/lib/document-intelligence.ts index 203a15bb..9be2e387 100644 --- a/dashboard/src/lib/document-intelligence.ts +++ b/dashboard/src/lib/document-intelligence.ts @@ -37,6 +37,13 @@ export type DocumentSandboxSession = { job_id?: string; }; +export type DocumentSandboxSessionRestore = { + upload: DocumentUploadState | null; + job: DocumentJobState | null; + uploadUnavailable: boolean; + jobUnavailable: boolean; +}; + export type DocumentJobProgress = { stage: "queued" | "preparing" | "extracting" | "validating" | "complete" | "attention" | "cancelled"; message: string; @@ -159,6 +166,18 @@ export function parseDocumentSandboxSession(value: string | null): DocumentSandb } } +export function resolveDocumentSandboxSessionRestore( + upload: PromiseSettledResult, + job: PromiseSettledResult, +): DocumentSandboxSessionRestore { + return { + upload: upload.status === "fulfilled" ? upload.value : null, + job: job.status === "fulfilled" ? job.value : null, + uploadUnavailable: upload.status === "rejected", + jobUnavailable: job.status === "rejected", + }; +} + export function serializeDocumentSandboxSession(session: DocumentSandboxSession): string { return JSON.stringify(session); }