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:
Tanya
2026-01-05 13:09:32 -05:00
parent 03d3a28b21
commit 0b95cd2492
8 changed files with 749 additions and 335 deletions
+1
View File
@@ -6,6 +6,7 @@ export enum JobStatus {
PROGRESS = 'progress',
SUCCESS = 'success',
FAILURE = 'failure',
CANCELLED = 'cancelled',
}
export interface JobResponse {
+66 -61
View File
@@ -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 && (