video-accessibility/frontend/src/hooks/useJob.ts
Vadym Samoilenko c1948ea198 feat(ux): T-2/PR-7/PR-8 — status color helper, queue stats widget, upload-final-VTT override
T-2: Extract getJobStatusColor() into utils/jobStatusMessages.ts; StatusBadge now uses the
     shared helper (single source of truth for badge colors).

PR-7: GET /admin/production/queue-stats — returns Celery queue depths via Redis LLEN.
      Production dashboard shows a live panel (10s refresh) with per-queue task counts.

PR-8: POST /admin/production/jobs/{id}/upload-final-vtt — Production/Admin can upload a
      hand-crafted VTT to bypass AI, writing to GCS and advancing the job to PENDING_QC.
      Upload modal added to FailuresList with language + type (captions/ad) selectors.

docker-compose.optical-dev.yml: enable USE_CELERY_FALLBACK=true, set worker replicas=1
      for all pipeline workers (ffmpeg/tts/whisper) with WORKER_CONCURRENCY=2 so the full
      pipeline runs on the 2-CPU optical-dev server until Cloud Run VPC Connector is ready.

Fix: remove unused effectiveMs variable in TimelinePreview (TS6133).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 11:12:36 +01:00

425 lines
No EOL
13 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../lib/api';
import type {
Job,
JobCreateRequest,
VttUpdateRequest,
BulkDeleteRequest,
BulkApproveRequest,
BulkReturnToQCRequest,
TTSPreferences,
AccessibleVideoMethod
} from '../types/api';
// Query hooks
export function useJobs(filters?: { status?: string; mine?: boolean; page?: number; size?: number }, options?: { enabled?: boolean }) {
return useQuery({
queryKey: ['jobs', filters],
queryFn: () => apiClient.getJobs(filters),
enabled: options?.enabled ?? true,
staleTime: 0, // Consider data stale immediately
refetchOnMount: 'always', // Always fetch fresh data when component mounts
});
}
export function useJob(jobId: string) {
return useQuery({
queryKey: ['jobs', jobId],
queryFn: () => apiClient.getJob(jobId),
enabled: !!jobId,
staleTime: 30000, // 30 seconds
refetchOnWindowFocus: false,
refetchInterval: (query) => {
const status = query.state.data?.status;
const processingStatuses = new Set([
'created', 'ingesting', 'ai_processing', 'translating',
'tts_generating', 'rendering_video', 'rendering_qc'
]);
return status && processingStatuses.has(status) ? 10000 : false;
},
});
}
const EARLY_STATUSES = new Set(['created', 'ingesting', 'ai_processing']);
export function useJobDownloads(jobId: string, jobStatus?: string) {
return useQuery({
queryKey: ['jobs', jobId, 'downloads'],
queryFn: () => apiClient.getJobDownloads(jobId),
enabled: !!jobId && (jobStatus === undefined || !EARLY_STATUSES.has(jobStatus)),
staleTime: 30000, // 30 seconds
refetchOnWindowFocus: false,
});
}
export function useJobVttContent(jobId: string, language?: string) {
return useQuery({
queryKey: ['jobs', jobId, 'vtt', language || 'source'],
queryFn: () => apiClient.getJobVttContent(jobId, language),
enabled: !!jobId,
staleTime: 30000, // 30 seconds
refetchOnWindowFocus: false,
});
}
export function useJobValidation(jobId: string) {
return useQuery({
queryKey: ['jobs', jobId, 'validation'],
queryFn: () => apiClient.validateJobAssets(jobId),
enabled: !!jobId,
});
}
// Mutation hooks
export function useCreateJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ data, file, onUploadProgress }: {
data: JobCreateRequest;
file: File;
onUploadProgress?: (progressEvent: { loaded: number; total: number }) => void
}) =>
apiClient.createJob(data, file, onUploadProgress),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useUpdateJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Job> }) =>
apiClient.updateJob(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useApproveEnglish() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes, tts_preferences, accessible_video_method }: {
id: string;
notes?: string;
tts_preferences?: TTSPreferences;
accessible_video_method?: AccessibleVideoMethod;
}) =>
apiClient.approveSource(id, notes, tts_preferences, accessible_video_method),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useApproveSource() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes, tts_preferences, accessible_video_method }: {
id: string;
notes?: string;
tts_preferences?: TTSPreferences;
accessible_video_method?: AccessibleVideoMethod;
}) =>
apiClient.approveSource(id, notes, tts_preferences, accessible_video_method),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useRejectJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes: string }) =>
apiClient.rejectJob(id, notes),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useCompleteJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes?: string }) =>
apiClient.completeJob(id, notes),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useUpdateJobVtt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: VttUpdateRequest }) =>
apiClient.updateJobVttContent(id, data),
onSuccess: (_, { id, data }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
// Invalidate the VTT cache for the specific language or 'source' if not specified
queryClient.invalidateQueries({ queryKey: ['jobs', id, 'vtt', data.language || 'source'] });
},
});
}
export function useRejectFinalReview() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes: string }) =>
apiClient.rejectFinalReview(id, notes),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useUpdateTTSPreferences() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, tts_preferences }: { id: string; tts_preferences: TTSPreferences }) =>
apiClient.updateTTSPreferences(id, tts_preferences),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
// Also invalidate accessible video edit state as TTS is being regenerated
queryClient.invalidateQueries({ queryKey: ['jobs', id, 'accessible-video'] });
},
});
}
export function useDeleteJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.deleteJob(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useBulkDeleteJobs() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: BulkDeleteRequest) => apiClient.bulkDeleteJobs(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useBulkApproveJobs() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: BulkApproveRequest) => apiClient.bulkApproveJobs(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useAdjustVttTiming() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, offsetSeconds, language = 'en', adjustCaptions = true, adjustAudioDescription = true }: {
id: string;
offsetSeconds: number;
language?: string;
adjustCaptions?: boolean;
adjustAudioDescription?: boolean;
}) =>
apiClient.adjustVttTiming(id, {
offset_seconds: offsetSeconds,
language,
adjust_captions: adjustCaptions,
adjust_audio_description: adjustAudioDescription,
}),
onSuccess: (_, { id, language }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs', id, 'vtt', language] });
},
});
}
export function useReprocessJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.reprocessJob(id),
onSuccess: (_, id) => {
// Invalidate both the specific job and the jobs list to reflect status changes
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useCloneJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.cloneJob(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useRetryTts() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.retryTts(id),
onSuccess: (_, id) => {
// Invalidate both the specific job and the jobs list to reflect status changes
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useRetryJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, fromStep }: { id: string; fromStep?: string }) =>
apiClient.retryJob(id, fromStep),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useReturnToQC() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, notes }: { id: string; notes: string }) =>
apiClient.returnToQC(id, { notes }),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['jobs', id] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useBulkReturnToQC() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: BulkReturnToQCRequest) => apiClient.bulkReturnToQC(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useBriefs() {
return useQuery({
queryKey: ['briefs'],
queryFn: () => apiClient.listBriefs(),
refetchInterval: 60_000,
});
}
export function useBrief(id: string) {
return useQuery({
queryKey: ['briefs', id],
queryFn: () => apiClient.getBrief(id),
enabled: !!id,
});
}
export function useCreateBrief() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: import('../types/api').JobBriefCreate) => apiClient.createBrief(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['briefs'] }),
});
}
export function useSubmitBrief() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.submitBrief(id),
onSuccess: (_, id) => {
queryClient.invalidateQueries({ queryKey: ['briefs', id] });
queryClient.invalidateQueries({ queryKey: ['briefs'] });
},
});
}
export function useApproveBrief() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiClient.approveBrief(id),
onSuccess: (_, id) => {
queryClient.invalidateQueries({ queryKey: ['briefs', id] });
queryClient.invalidateQueries({ queryKey: ['briefs'] });
},
});
}
export function useFailures(filters?: { step?: string; org_id?: string; limit?: number; skip?: number }) {
return useQuery({
queryKey: ['failures', filters],
queryFn: () => apiClient.listFailures(filters),
refetchInterval: 30_000,
});
}
export function useBulkRetry() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ job_ids, strategy }: { job_ids: string[]; strategy?: 'auto' | 'from_scratch' }) =>
apiClient.bulkRetry(job_ids, strategy),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['failures'] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
export function useProductionQueueStats() {
return useQuery({
queryKey: ['production-queue-stats'],
queryFn: () => apiClient.getProductionQueueStats(),
refetchInterval: 10_000,
});
}
export function useUploadFinalVtt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
jobId, language, vttFile, vttType,
}: { jobId: string; language: string; vttFile: File; vttType?: 'captions' | 'ad' }) =>
apiClient.uploadFinalVtt(jobId, language, vttFile, vttType),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['failures'] });
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}