From f85d6a6b51003dfecfaea3d3f1360c3e08e18089 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 23 Mar 2026 15:05:23 +0000 Subject: [PATCH] fix: repair brief upload and real-time job progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs fixed: 1. api/jobs.ts: remove manual Content-Type header on FormData upload. Setting it without the multipart boundary caused Quart to reject the request body — the root cause of brief upload failures. 2. progress.py: include full job.to_dict() in job.progress / job.completed / job.failed WebSocket messages. Frontend checks msg.job to call updateJob() — without it, job cards never updated in real-time. 3. AppShell: move useWebSocket() here from BriefUploadPage so the WS connection persists across all pages, not just the upload page. Co-Authored-By: Claude Sonnet 4.6 --- backend/server/runners/progress.py | 15 +++++++++------ frontend/src/api/jobs.ts | 6 +++--- frontend/src/components/layout/AppShell.tsx | 4 ++++ frontend/src/pages/BriefUploadPage.tsx | 3 --- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/server/runners/progress.py b/backend/server/runners/progress.py index b870abd..0d00289 100755 --- a/backend/server/runners/progress.py +++ b/backend/server/runners/progress.py @@ -46,7 +46,7 @@ class ProgressReporter: self.job.add_log('INFO', message) self.logger.info(message) - # Broadcast progress update + # Broadcast progress update — include full job so frontend can updateJob() await self.ws_manager.broadcast_job_update(self.job.id, { 'type': 'job.progress', 'jobId': self.job.id, @@ -54,7 +54,8 @@ class ProgressReporter: 'progressPct': progress_pct, 'message': message, 'stepLabel': self.job.step_label, - 'providerUpdates': {k: v.to_dict() for k, v in self.job.provider_updates.items()} + 'providerUpdates': {k: v.to_dict() for k, v in self.job.provider_updates.items()}, + 'job': self.job.to_dict(), }) self.logger.debug(f"Progress update: {phase.value if hasattr(phase, 'value') else phase} {progress_pct}% - {message}") @@ -186,12 +187,13 @@ class ProgressReporter: try: self.job.add_log('INFO', 'Processing completed successfully') - # Broadcast completion + # Broadcast completion — include full job so frontend can updateJob() await self.ws_manager.broadcast_job_update(self.job.id, { 'type': 'job.completed', 'jobId': self.job.id, 'resultCsvUrl': result_csv_url, - 'summary': summary_data + 'summary': summary_data, + 'job': self.job.to_dict(), }) self.logger.info(f"Job {self.job.id} completed successfully") @@ -210,11 +212,12 @@ class ProgressReporter: self.job.mark_failed(error) self.job.add_log('ERROR', f'Processing failed: {error}') - # Broadcast failure + # Broadcast failure — include full job so frontend can updateJob() await self.ws_manager.broadcast_job_update(self.job.id, { 'type': 'job.failed', 'jobId': self.job.id, - 'error': error + 'error': error, + 'job': self.job.to_dict(), }) self.logger.error(f"Job {self.job.id} failed: {error}") diff --git a/frontend/src/api/jobs.ts b/frontend/src/api/jobs.ts index 9b7924c..6187bf0 100644 --- a/frontend/src/api/jobs.ts +++ b/frontend/src/api/jobs.ts @@ -11,9 +11,9 @@ export const createJob = (files: File[], modelConfig?: ModelConfiguration) => { const form = new FormData() files.forEach((f, i) => form.append(`file_${i}`, f)) if (modelConfig) form.append('modelConfig', JSON.stringify(modelConfig)) - return api.post<{ jobs: Job[] }>('/jobs', form, { - headers: { 'Content-Type': 'multipart/form-data' }, - }).then(r => r.data.jobs) + // Do NOT set Content-Type manually — axios sets it automatically with the + // correct multipart boundary when given a FormData instance. + return api.post<{ jobs: Job[] }>('/jobs', form).then(r => r.data.jobs) } export const deleteJob = (id: string) => api.delete(`/jobs/${id}`) diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx index f57da38..8737b10 100644 --- a/frontend/src/components/layout/AppShell.tsx +++ b/frontend/src/components/layout/AppShell.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react' import Sidebar from './Sidebar' import TopBar from './TopBar' import { useSheetStore } from '../../stores/useSheetStore' +import { useWebSocket } from '../../hooks/useWebSocket' interface Props { children: React.ReactNode @@ -11,6 +12,9 @@ export default function AppShell({ children }: Props) { const fetchSheets = useSheetStore(s => s.fetchSheets) useEffect(() => { fetchSheets() }, []) + // WebSocket mounted here so job updates persist across all pages + useWebSocket() + return (
diff --git a/frontend/src/pages/BriefUploadPage.tsx b/frontend/src/pages/BriefUploadPage.tsx index 5cf385b..acc24b7 100644 --- a/frontend/src/pages/BriefUploadPage.tsx +++ b/frontend/src/pages/BriefUploadPage.tsx @@ -3,15 +3,12 @@ import { useNavigate } from 'react-router-dom' import { useJobStore } from '../stores/useJobStore' import FileDropzone from '../components/brief/FileDropzone' import JobProgressCard from '../components/brief/JobProgressCard' -import { useWebSocket } from '../hooks/useWebSocket' import toast from 'react-hot-toast' export default function BriefUploadPage() { const navigate = useNavigate() const { jobs, uploadFiles, deleteJob, fetchJobs, loading } = useJobStore() - useWebSocket() - useEffect(() => { fetchJobs() }, []) const handleFiles = async (files: File[]) => {