fix: repair brief upload and real-time job progress

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 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-23 15:05:23 +00:00
parent 45c6b2e720
commit f85d6a6b51
4 changed files with 16 additions and 12 deletions

View file

@ -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}")

View file

@ -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}`)

View file

@ -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 (
<div className="flex h-screen overflow-hidden" style={{ background: 'var(--bg-color)' }}>
<Sidebar />

View file

@ -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[]) => {