feat: Add job cancellation support and update job status handling
This commit introduces a new `CANCELLED` status to the job management system, allowing users to cancel ongoing jobs. The frontend is updated to handle job cancellation requests, providing user feedback during the cancellation process. Additionally, the backend is enhanced to manage job statuses more effectively, ensuring that jobs can be marked as cancelled and that appropriate messages are displayed to users. This improvement enhances the overall user experience by providing better control over job processing.
This commit is contained in:
@@ -6,6 +6,7 @@ export enum JobStatus {
|
||||
PROGRESS = 'progress',
|
||||
SUCCESS = 'success',
|
||||
FAILURE = 'failure',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export interface JobResponse {
|
||||
|
||||
@@ -25,11 +25,6 @@ export default function Process() {
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [currentJob, setCurrentJob] = useState<JobResponse | null>(null)
|
||||
const [jobProgress, setJobProgress] = useState<JobProgress | null>(null)
|
||||
const [processingResult, setProcessingResult] = useState<{
|
||||
photos_processed?: number
|
||||
faces_detected?: number
|
||||
faces_stored?: number
|
||||
} | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
|
||||
@@ -45,7 +40,6 @@ export default function Process() {
|
||||
const handleStartProcessing = async () => {
|
||||
setIsProcessing(true)
|
||||
setError(null)
|
||||
setProcessingResult(null)
|
||||
setCurrentJob(null)
|
||||
setJobProgress(null)
|
||||
|
||||
@@ -79,21 +73,38 @@ export default function Process() {
|
||||
}
|
||||
|
||||
const handleStopProcessing = async () => {
|
||||
if (!currentJob) {
|
||||
// Use jobProgress if currentJob is not available (might happen if status is still Pending)
|
||||
const jobId = currentJob?.id || jobProgress?.id
|
||||
|
||||
if (!jobId) {
|
||||
console.error('Cannot stop: No job ID available')
|
||||
setError('Cannot stop: No active job found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[Process] STOP button clicked for job ${jobId}`)
|
||||
|
||||
try {
|
||||
// Call API to cancel the job
|
||||
const result = await jobsApi.cancelJob(currentJob.id)
|
||||
console.log('Job cancellation requested:', result)
|
||||
console.log(`[Process] Calling cancelJob API for job ${jobId}`)
|
||||
const result = await jobsApi.cancelJob(jobId)
|
||||
console.log('[Process] Job cancellation requested:', result)
|
||||
|
||||
// Update job status to show cancellation is in progress
|
||||
setCurrentJob({
|
||||
...currentJob,
|
||||
status: JobStatus.PROGRESS,
|
||||
message: 'Cancellation requested - finishing current photo...',
|
||||
})
|
||||
if (currentJob) {
|
||||
setCurrentJob({
|
||||
...currentJob,
|
||||
status: JobStatus.PROGRESS,
|
||||
message: 'Cancellation requested - finishing current photo...',
|
||||
})
|
||||
} else if (jobProgress) {
|
||||
// If currentJob is not set, update jobProgress
|
||||
setJobProgress({
|
||||
...jobProgress,
|
||||
status: 'progress',
|
||||
message: 'Cancellation requested - finishing current photo...',
|
||||
})
|
||||
}
|
||||
|
||||
// Don't close SSE stream yet - keep it open to wait for job to actually stop
|
||||
// The job will finish the current photo, then stop and send a final status update
|
||||
@@ -103,8 +114,14 @@ export default function Process() {
|
||||
// This will be checked in the SSE handler
|
||||
setError(null) // Clear any previous errors
|
||||
} catch (err: any) {
|
||||
console.error('Error cancelling job:', err)
|
||||
setError(err.response?.data?.detail || err.message || 'Failed to cancel job')
|
||||
console.error('[Process] Error cancelling job:', err)
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to cancel job'
|
||||
setError(errorMessage)
|
||||
console.error('[Process] Full error details:', {
|
||||
message: err.message,
|
||||
response: err.response?.data,
|
||||
status: err.response?.status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +146,7 @@ export default function Process() {
|
||||
progress: JobStatus.PROGRESS,
|
||||
success: JobStatus.SUCCESS,
|
||||
failure: JobStatus.FAILURE,
|
||||
cancelled: JobStatus.CANCELLED,
|
||||
}
|
||||
|
||||
const jobStatus = statusMap[data.status] || JobStatus.PENDING
|
||||
@@ -148,18 +166,29 @@ export default function Process() {
|
||||
}
|
||||
|
||||
// Check if job is complete
|
||||
if (jobStatus === JobStatus.SUCCESS || jobStatus === JobStatus.FAILURE) {
|
||||
if (jobStatus === JobStatus.SUCCESS || jobStatus === JobStatus.FAILURE || jobStatus === JobStatus.CANCELLED) {
|
||||
setIsProcessing(false)
|
||||
eventSource.close()
|
||||
eventSourceRef.current = null
|
||||
|
||||
// Show cancellation message if job was cancelled
|
||||
if (data.message && (data.message.includes('Cancelled') || data.message.includes('cancelled'))) {
|
||||
setError(`Job cancelled: ${data.message}`)
|
||||
// Handle cancelled jobs
|
||||
if (jobStatus === JobStatus.CANCELLED) {
|
||||
const progressInfo = data.processed !== undefined && data.total !== undefined
|
||||
? ` (processed ${data.processed} of ${data.total} photos)`
|
||||
: ''
|
||||
setError(`Processing stopped: ${data.message || 'Cancelled by user'}${progressInfo}`)
|
||||
}
|
||||
// Show error message for failures
|
||||
else if (jobStatus === JobStatus.FAILURE) {
|
||||
// Show failure message with progress info if available
|
||||
const progressInfo = data.processed !== undefined && data.total !== undefined
|
||||
? ` (processed ${data.processed} of ${data.total} photos)`
|
||||
: ''
|
||||
setError(`Processing failed: ${data.message || 'Unknown error'}${progressInfo}`)
|
||||
}
|
||||
|
||||
// Fetch final job result to get processing stats
|
||||
if (jobStatus === JobStatus.SUCCESS) {
|
||||
// Fetch final job result to get processing stats for successful or cancelled jobs
|
||||
if (jobStatus === JobStatus.SUCCESS || jobStatus === JobStatus.CANCELLED) {
|
||||
fetchJobResult(jobId)
|
||||
}
|
||||
}
|
||||
@@ -191,15 +220,6 @@ export default function Process() {
|
||||
try {
|
||||
const job = await jobsApi.getJob(jobId)
|
||||
setCurrentJob(job)
|
||||
|
||||
// Extract result data from job progress
|
||||
if (jobProgress) {
|
||||
setProcessingResult({
|
||||
photos_processed: jobProgress.processed,
|
||||
faces_detected: jobProgress.faces_detected,
|
||||
faces_stored: jobProgress.faces_stored,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching job result:', err)
|
||||
}
|
||||
@@ -347,8 +367,16 @@ export default function Process() {
|
||||
{isProcessing && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStopProcessing}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log('[Process] STOP button clicked, isProcessing:', isProcessing)
|
||||
console.log('[Process] currentJob:', currentJob)
|
||||
console.log('[Process] jobProgress:', jobProgress)
|
||||
handleStopProcessing()
|
||||
}}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!currentJob && !jobProgress}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
@@ -375,8 +403,11 @@ export default function Process() {
|
||||
>
|
||||
{currentJob.status === JobStatus.SUCCESS && '✓ '}
|
||||
{currentJob.status === JobStatus.FAILURE && '✗ '}
|
||||
{currentJob.status.charAt(0).toUpperCase() +
|
||||
currentJob.status.slice(1)}
|
||||
{currentJob.status === JobStatus.CANCELLED && '⏹ '}
|
||||
{currentJob.status === JobStatus.CANCELLED
|
||||
? 'Stopped'
|
||||
: currentJob.status.charAt(0).toUpperCase() +
|
||||
currentJob.status.slice(1)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentJob.progress}%
|
||||
@@ -415,32 +446,6 @@ export default function Process() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results Section */}
|
||||
{processingResult && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Processing Results
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
{processingResult.photos_processed !== undefined && (
|
||||
<p className="text-green-600">
|
||||
✓ {processingResult.photos_processed} photos processed
|
||||
</p>
|
||||
)}
|
||||
{processingResult.faces_detected !== undefined && (
|
||||
<p className="text-gray-600">
|
||||
{processingResult.faces_detected} faces detected
|
||||
</p>
|
||||
)}
|
||||
{processingResult.faces_stored !== undefined && (
|
||||
<p className="text-gray-700 font-medium">
|
||||
{processingResult.faces_stored} faces stored in database
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Section */}
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user