From 46b41d254e36b996a91de73ed69ed7c928d55279 Mon Sep 17 00:00:00 2001 From: Nishit24113 Date: Tue, 9 Jun 2026 09:43:31 -0600 Subject: [PATCH 1/2] Surface backend processing failures to the user The PDF pipeline now writes result/FAILED_.json on any failure (with a reason category and the affected page range). Previously the UI only polled for result/COMPLIANT_ and ignored failures, so a failed job left the user watching the spinner until the 30-minute polling timeout. ProcessingContainer now, on each poll (PDF format), checks for the FAILED_ marker before the COMPLIANT check. If found it stops polling and shows a failure card with the summary, affected pages, and reason code, plus a "Try another document" action. --- pdf_ui/src/components/ProcessingContainer.css | 60 ++++++++++++++ pdf_ui/src/components/ProcessingContainer.jsx | 81 +++++++++++++++++-- 2 files changed, 135 insertions(+), 6 deletions(-) diff --git a/pdf_ui/src/components/ProcessingContainer.css b/pdf_ui/src/components/ProcessingContainer.css index 95b203a..faab2f2 100644 --- a/pdf_ui/src/components/ProcessingContainer.css +++ b/pdf_ui/src/components/ProcessingContainer.css @@ -459,3 +459,63 @@ width: 100%; } } + +/* Failure state shown when the backend writes result/FAILED_.json */ +.failure-section { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 32px 24px; + background: #fdf2f2; + border: 1px solid #f5c6cb; + border-radius: 12px; + margin-top: 8px; +} + +.failure-icon { + font-size: 40px; + line-height: 1; + margin-bottom: 12px; +} + +.failure-title { + margin: 0 0 8px; + color: #842029; + font-size: 20px; +} + +.failure-summary { + margin: 0 0 8px; + color: #5c1a1f; + font-size: 15px; + max-width: 520px; +} + +.failure-pages { + margin: 0 0 4px; + color: #842029; + font-weight: 600; + font-size: 14px; +} + +.failure-reason { + margin: 0 0 16px; + color: #9a6b6f; + font-size: 12px; + font-family: monospace; +} + +.failure-retry-btn { + background: #c0392b; + color: #fff; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 14px; + cursor: pointer; +} + +.failure-retry-btn:hover { + background: #a93226; +} diff --git a/pdf_ui/src/components/ProcessingContainer.jsx b/pdf_ui/src/components/ProcessingContainer.jsx index 20c76bc..a84c272 100644 --- a/pdf_ui/src/components/ProcessingContainer.jsx +++ b/pdf_ui/src/components/ProcessingContainer.jsx @@ -18,6 +18,10 @@ const ProcessingContainer = ({ const [currentStep, setCurrentStep] = useState(0); const [elapsedTime, setElapsedTime] = useState(0); const [pollingAttempts, setPollingAttempts] = useState(0); + // Set when the backend reports a processing failure via result/FAILED_.json. + // When present, polling stops and a failure message (with reason) is shown instead + // of leaving the user waiting until the polling timeout. + const [failureInfo, setFailureInfo] = useState(null); const processingSteps = [ { title: "Analyzing Document Structure", description: "Scanning PDF for accessibility issues" }, @@ -156,6 +160,53 @@ const ProcessingContainer = ({ console.log(`🔍 Polling attempt ${pollingAttempts + 1}/${MAX_POLLING_ATTEMPTS} for object key:`, objectKey); + // Before checking for success, check whether the backend reported a failure. + // The PDF pipeline writes result/FAILED_.json (name without extension) + // on any failure. If present, stop polling and surface the reason to the user + // instead of spinning until the polling timeout. (PDF format only; the HTML + // pipeline does not emit this marker.) + if (selectedFormat !== 'html') { + const failedKey = `result/FAILED_${updatedFilename.replace(/\.pdf$/i, '')}.json`; + try { + const failedObject = await s3.send( + new GetObjectCommand({ + Bucket: selectedBucket, + Key: failedKey, + }) + ); + const failedBody = await failedObject.Body.transformToString(); + let parsed = {}; + try { + parsed = JSON.parse(failedBody); + } catch (parseErr) { + console.warn('Could not parse failure marker JSON:', parseErr); + } + + console.error('❌ Backend reported a processing failure:', parsed); + + // Build a user-facing list of failed pages, if available. + const pages = (parsed.failed_chunks || []) + .filter(c => c.page_start && c.page_end) + .map(c => `pages ${c.page_start}-${c.page_end}`) + .join(', '); + + setFailureInfo({ + summary: parsed.summary || 'Processing failed. Please try again or contact support.', + reasonCategory: parsed.reason_category || 'UNKNOWN', + pages, + }); + + // Stop all polling/animation; a failure is terminal. + clearInterval(intervalId); + clearInterval(timeIntervalId); + clearInterval(stepIntervalId); + return; + } catch (failedCheckErr) { + // No failure marker yet (404) — this is the normal case; continue to + // the success check below. + } + } + // Check if the processed file exists await s3.send( new HeadObjectCommand({ @@ -191,7 +242,8 @@ const ProcessingContainer = ({ } }; - if (updatedFilename && !isFileReady) { + // Do not (re)start polling once the file is ready or a failure was reported. + if (updatedFilename && !isFileReady && !failureInfo) { // Reset polling attempts for new file setPollingAttempts(0); @@ -214,7 +266,7 @@ const ProcessingContainer = ({ clearInterval(timeIntervalId); clearInterval(stepIntervalId); }; - }, [updatedFilename, isFileReady, onFileReady, awsCredentials, originalFileName, selectedFormat, generatePresignedUrl, processingSteps.length, pollingAttempts]); + }, [updatedFilename, isFileReady, onFileReady, awsCredentials, originalFileName, selectedFormat, generatePresignedUrl, processingSteps.length, pollingAttempts, failureInfo]); return ( @@ -234,14 +286,31 @@ const ProcessingContainer = ({ ⏱️ Time elapsed: {formatElapsedTime(elapsedTime)}

- {isFileReady - ? 'Remediation complete! Your file is ready for download.' - : 'Remediation process typically takes a few minutes to complete depending on the document complexity' + {failureInfo + ? 'We were unable to remediate this document.' + : isFileReady + ? 'Remediation complete! Your file is ready for download.' + : 'Remediation process typically takes a few minutes to complete depending on the document complexity' }

- {!isFileReady ? ( + {failureInfo ? ( +
+
⚠️
+

Remediation Failed

+

{failureInfo.summary}

+ {failureInfo.pages && ( +

Affected: {failureInfo.pages}

+ )} +

Reason code: {failureInfo.reasonCategory}

+ {onNewUpload && ( + + )} +
+ ) : !isFileReady ? (
{processingSteps.map((step, index) => ( From 3f84fd400c63999be8d8d47240ab1ad39e92ecef Mon Sep 17 00:00:00 2001 From: Nishit24113 Date: Wed, 17 Jun 2026 18:37:33 -0600 Subject: [PATCH 2/2] fix: show timeout error to user instead of silently stopping polling After 30 minutes (120 attempts) the UI now sets a failure state with a clear message instead of clearing the interval with no user feedback. --- pdf_ui/src/components/ProcessingContainer.jsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pdf_ui/src/components/ProcessingContainer.jsx b/pdf_ui/src/components/ProcessingContainer.jsx index a84c272..aff8421 100644 --- a/pdf_ui/src/components/ProcessingContainer.jsx +++ b/pdf_ui/src/components/ProcessingContainer.jsx @@ -78,6 +78,7 @@ const ProcessingContainer = ({ let intervalId; let timeIntervalId; let stepIntervalId; + let attemptCount = 0; const checkFileAvailability = async () => { // Maximum polling time: 30 minutes (120 attempts * 15 seconds = 30 minutes) @@ -85,20 +86,22 @@ const ProcessingContainer = ({ try { // Increment polling attempts - setPollingAttempts(prev => { - const newAttempts = prev + 1; + attemptCount += 1; + setPollingAttempts(attemptCount); - // Stop polling after maximum attempts - if (newAttempts >= MAX_POLLING_ATTEMPTS) { - console.warn('⚠️ Maximum polling attempts reached. Stopping file check.'); - clearInterval(intervalId); - clearInterval(timeIntervalId); - clearInterval(stepIntervalId); - return newAttempts; - } - - return newAttempts; - }); + // Stop polling after maximum attempts and show a timeout message to the user. + if (attemptCount >= MAX_POLLING_ATTEMPTS) { + console.warn('⚠️ Maximum polling attempts reached. Stopping file check.'); + clearInterval(intervalId); + clearInterval(timeIntervalId); + clearInterval(stepIntervalId); + setFailureInfo({ + summary: 'Processing timed out after 30 minutes. Please try uploading again or contact support if the issue persists.', + reasonCategory: 'TIMEOUT', + pages: '', + }); + return; + } // Select the correct bucket based on format (same logic as UploadSection) const selectedBucket = selectedFormat === 'html' ? HTMLBucket : PDFBucket;