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:
parent
45c6b2e720
commit
f85d6a6b51
4 changed files with 16 additions and 12 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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[]) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue