import { useState, useEffect, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; import { Search, RefreshCw, Trash2, Ban, Clock, X, Info, Activity, Database, Zap, Maximize2, MoreHorizontal, ArrowDown, ArrowUp, AlertCircle } from "lucide-react"; import { apiAction, apiJson, isApiError } from "../lib/api"; import { useDebouncedValue } from "../lib/useDebouncedValue"; import { showToast } from "../lib/toast"; import ConfirmDialog from "./ui/ConfirmDialog"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { motion, AnimatePresence } from "framer-motion"; import { withErrorBoundary } from "./ErrorBoundary"; function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } function focusableElements(root: HTMLElement): HTMLElement[] { const selector = [ "a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])", ].join(","); return Array.from(root.querySelectorAll(selector)).filter( (element) => !element.hasAttribute("disabled") ); } export interface SkipDetail { summary: string; detail: string; action: string | null; measured: Record; } function formatReductionPercent(value?: string): string { if (!value) { return "?"; } const parsed = Number.parseFloat(value); return Number.isFinite(parsed) ? `${(parsed * 100).toFixed(0)}%` : value; } export function humanizeSkipReason(reason: string): SkipDetail { const pipeIdx = reason.indexOf("|"); const key = pipeIdx === -1 ? reason.trim() : reason.slice(0, pipeIdx).trim(); const paramStr = pipeIdx === -1 ? "" : reason.slice(pipeIdx + 1); const measured: Record = {}; for (const pair of paramStr.split(",")) { const [rawKey, ...rawValueParts] = pair.split("="); if (!rawKey || rawValueParts.length === 0) { continue; } measured[rawKey.trim()] = rawValueParts.join("=").trim(); } const fallbackDisabledMatch = key.match( /^Preferred codec\s+(.+?)\s+unavailable and fallback disabled$/i ); if (fallbackDisabledMatch) { measured.codec ??= fallbackDisabledMatch[1]; return { summary: "Preferred encoder unavailable", detail: `The preferred codec (${measured.codec ?? "target codec"}) is not available and CPU fallback is disabled in settings.`, action: "Go to Settings -> Hardware and enable CPU fallback, or check that your GPU encoder is working correctly.", measured, }; } switch (key) { case "analysis_failed": return { summary: "File could not be analyzed", detail: `FFprobe failed to read this file. It may be corrupt, incomplete, or in an unsupported format. Error: ${measured.error ?? "unknown"}`, action: "Try playing the file in VLC or another media player. If it plays fine, re-run the scan. If not, the file may be damaged.", measured, }; case "planning_failed": return { summary: "Transcoding plan could not be created", detail: `An internal error occurred while planning the transcode for this file. This is likely a bug. Error: ${measured.error ?? "unknown"}`, action: "Check the logs below for details. If this happens repeatedly, please report it as a bug.", measured, }; case "already_target_codec": return { summary: "Already in target format", detail: `This file is already encoded as ${measured.codec ?? "the target codec"}${measured.bit_depth ? ` at ${measured.bit_depth}-bit` : ""}. Re-encoding would waste time and could reduce quality.`, action: null, measured, }; case "already_target_codec_wrong_container": return { summary: "Target codec, wrong container", detail: `The video is already in the right codec but wrapped in a ${measured.container ?? "MP4"} container. Alchemist will remux it to ${measured.target_extension ?? "MKV"} - fast and lossless, no quality loss.`, action: null, measured, }; case "bpp_below_threshold": return { summary: "Already efficiently compressed", detail: `Bits-per-pixel (${measured.bpp ?? "?"}) is below the minimum threshold (${measured.threshold ?? "?"}). This file is already well-compressed - transcoding it would spend significant time for minimal space savings.`, action: "If you want to force transcoding, lower the BPP threshold in Settings -> Transcoding.", measured, }; case "below_min_file_size": return { summary: "File too small to process", detail: `File size (${measured.size_mb ?? "?"}MB) is below the minimum threshold (${measured.threshold_mb ?? "?"}MB). Small files aren't worth the transcoding overhead.`, action: "Lower the minimum file size threshold in Settings -> Transcoding if you want small files processed.", measured, }; case "size_reduction_insufficient": return { summary: "Not enough space would be saved", detail: `The predicted size reduction (${formatReductionPercent(measured.predicted)}) is below the required threshold (${formatReductionPercent(measured.threshold)}). Transcoding this file wouldn't recover meaningful storage.`, action: "Lower the size reduction threshold in Settings -> Transcoding to encode files with smaller savings.", measured, }; case "no_suitable_encoder": return { summary: "No encoder available", detail: `No encoder was found for ${measured.codec ?? "the target codec"}. Hardware detection may have failed, or CPU fallback is disabled.`, action: "Check Settings -> Hardware. Enable CPU fallback, or verify your GPU is detected correctly.", measured, }; case "Output path matches input path": return { summary: "Output would overwrite source", detail: "The configured output path is the same as the source file. Alchemist refused to proceed to avoid overwriting your original file.", action: "Go to Settings -> Files and configure a different output suffix or output folder.", measured, }; case "Output already exists": return { summary: "Output file already exists", detail: "A transcoded version of this file already exists at the output path. Alchemist skipped it to avoid duplicating work.", action: "If you want to re-transcode it, delete the existing output file first, then retry the job.", measured, }; case "incomplete_metadata": return { summary: "Missing file metadata", detail: `FFprobe could not determine the ${measured.missing ?? "required metadata"} for this file. Without reliable metadata Alchemist cannot make a valid transcoding decision.`, action: "Run a Library Doctor scan to check if this file is corrupt. Try playing it in a media player to confirm it is readable.", measured, }; case "already_10bit": return { summary: "Already 10-bit", detail: "This file is already encoded in high-quality 10-bit depth. Re-encoding it could reduce quality.", action: null, measured, }; case "remux: mp4_to_mkv_stream_copy": return { summary: "Remuxed (no re-encode)", detail: "This file was remuxed from MP4 to MKV using stream copy - fast and lossless. No quality was lost.", action: null, measured, }; case "Low quality (VMAF)": return { summary: "Quality check failed", detail: "The encoded file scored below the minimum VMAF quality threshold. Alchemist rejected the output to protect quality.", action: "The original file has been preserved. You can lower the VMAF threshold in Settings -> Quality, or disable VMAF checking entirely.", measured, }; default: return { summary: "Decision recorded", detail: reason, action: null, measured, }; } } function explainFailureSummary(summary: string): string { const normalized = summary.toLowerCase(); if (normalized.includes("cancelled")) { return "This job was cancelled before encoding completed. The original file is untouched."; } if (normalized.includes("no such file or directory")) { return "The source file could not be found. It may have been moved or deleted."; } if (normalized.includes("invalid data found") || normalized.includes("moov atom not found")) { return "This file appears to be corrupt or incomplete. Try running a Library Doctor scan."; } if (normalized.includes("permission denied")) { return "Alchemist doesn't have permission to read this file. Check the file permissions."; } if (normalized.includes("encoder not found") || normalized.includes("unknown encoder")) { return "The required encoder is not available in your FFmpeg installation."; } if (normalized.includes("out of memory") || normalized.includes("cannot allocate memory")) { return "The system ran out of memory during encoding. Try reducing concurrent jobs."; } if (normalized.includes("transcode_failed") || normalized.includes("ffmpeg exited")) { return "FFmpeg failed during encoding. This is often caused by a corrupt source file or an encoder configuration issue. Check the logs below for the specific FFmpeg error."; } if (normalized.includes("probing failed")) { return "FFprobe could not read this file. It may be corrupt or in an unsupported format."; } if (normalized.includes("planning_failed") || normalized.includes("planner")) { return "An error occurred while planning the transcode. Check the logs below for details."; } if (normalized.includes("output_size=0") || normalized.includes("output was empty")) { return "Encoding produced an empty output file. This usually means FFmpeg crashed silently. Check the logs below for FFmpeg output."; } if ( normalized.includes("videotoolbox") || normalized.includes("vt_compression") || normalized.includes("err=-12902") || normalized.includes("mediaserverd") || normalized.includes("no capable devices") ) { return "The VideoToolbox hardware encoder failed. This can happen when the GPU is busy, the file uses an unsupported pixel format, or macOS Media Services are unavailable. Retry the job — if it keeps failing, CPU fallback is available in Settings → Hardware."; } if ( normalized.includes("encoder fallback") || normalized.includes("fallback detected") ) { return "The hardware encoder was unavailable and fell back to software encoding, which was not allowed by your settings. Enable CPU fallback in Settings → Hardware, or retry when the GPU is less busy."; } if (normalized.includes("ffmpeg failed")) { return "FFmpeg failed during encoding. Check the logs below for the specific error. Common causes: unsupported pixel format, codec not available, or corrupt source file."; } return summary; } function logLevelClass(level: string): string { switch (level.toLowerCase()) { case "error": return "text-status-error"; case "warn": case "warning": return "text-helios-solar"; default: return "text-helios-slate"; } } interface Job { id: number; input_path: string; output_path: string; status: string; priority: number; progress: number; created_at: string; updated_at: string; attempt_count: number; vmaf_score?: number; decision_reason?: string; encoder?: string; } function retryCountdown(job: Job): string | null { if (job.status !== "failed") return null; if (!job.attempt_count || job.attempt_count === 0) return null; const backoffMins = job.attempt_count === 1 ? 5 : job.attempt_count === 2 ? 15 : job.attempt_count === 3 ? 60 : 360; const updatedMs = new Date(job.updated_at).getTime(); const retryAtMs = updatedMs + backoffMins * 60 * 1000; const remainingMs = retryAtMs - Date.now(); if (remainingMs <= 0) return "Retrying soon"; const remainingMins = Math.ceil(remainingMs / 60_000); if (remainingMins < 60) return `Retrying in ${remainingMins}m`; const hrs = Math.floor(remainingMins / 60); const mins = remainingMins % 60; return mins > 0 ? `Retrying in ${hrs}h ${mins}m` : `Retrying in ${hrs}h`; } interface JobMetadata { duration_secs: number; codec_name: string; width: number; height: number; bit_depth?: number; size_bytes: number; video_bitrate_bps?: number; container_bitrate_bps?: number; fps: number; container: string; audio_codec?: string; audio_channels?: number; dynamic_range?: string; } interface EncodeStats { input_size_bytes: number; output_size_bytes: number; compression_ratio: number; encode_time_seconds: number; encode_speed: number; avg_bitrate_kbps: number; vmaf_score?: number; } interface LogEntry { id: number; level: string; message: string; created_at: string; } interface JobDetail { job: Job; metadata: JobMetadata | null; encode_stats: EncodeStats | null; job_logs: LogEntry[]; job_failure_summary: string | null; } interface CountMessageResponse { count: number; message: string; } type TabType = "all" | "active" | "queued" | "completed" | "failed" | "skipped" | "archived"; type SortField = "updated_at" | "created_at" | "input_path" | "size"; const SORT_OPTIONS: Array<{ value: SortField; label: string }> = [ { value: "updated_at", label: "Last Updated" }, { value: "created_at", label: "Date Added" }, { value: "input_path", label: "File Name" }, { value: "size", label: "File Size" }, ]; function JobManager() { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState>(new Set()); const [activeTab, setActiveTab] = useState("all"); const [searchInput, setSearchInput] = useState(""); const debouncedSearch = useDebouncedValue(searchInput, 350); const [page, setPage] = useState(1); const [sortBy, setSortBy] = useState("updated_at"); const [sortDesc, setSortDesc] = useState(true); const [refreshing, setRefreshing] = useState(false); const [focusedJob, setFocusedJob] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [actionError, setActionError] = useState(null); const [menuJobId, setMenuJobId] = useState(null); const menuRef = useRef(null); const detailDialogRef = useRef(null); const detailLastFocusedRef = useRef(null); const confirmOpenRef = useRef(false); const encodeStartTimes = useRef>(new Map()); const [confirmState, setConfirmState] = useState<{ title: string; body: string; confirmLabel: string; confirmTone?: "danger" | "primary"; onConfirm: () => Promise | void; } | null>(null); const [tick, setTick] = useState(0); useEffect(() => { const id = window.setInterval(() => setTick(t => t + 1), 30_000); return () => window.clearInterval(id); }, []); const isJobActive = (job: Job) => ["analyzing", "encoding", "remuxing", "resuming"].includes(job.status); const formatJobActionError = (error: unknown, fallback: string) => { if (!isApiError(error)) { return fallback; } const blocked = Array.isArray((error.body as { blocked?: unknown } | undefined)?.blocked) ? ((error.body as { blocked?: Array<{ id?: number; status?: string }> }).blocked ?? []) : []; if (blocked.length === 0) { return error.message; } const summary = blocked .map((job) => `#${job.id ?? "?"} (${job.status ?? "unknown"})`) .join(", "); return `${error.message}: ${summary}`; }; // Filter mapping const getStatusFilter = (tab: TabType) => { switch (tab) { case "active": return ["analyzing", "encoding", "remuxing", "resuming"]; case "queued": return ["queued"]; case "completed": return ["completed"]; case "failed": return ["failed", "cancelled"]; case "skipped": return ["skipped"]; default: return []; } }; const fetchJobs = useCallback(async (silent = false) => { if (!silent) { setRefreshing(true); } try { const params = new URLSearchParams({ limit: "50", page: page.toString(), sort: sortBy, sort_desc: String(sortDesc), archived: String(activeTab === "archived"), }); params.set("sort_by", sortBy); const statusFilter = getStatusFilter(activeTab); if (statusFilter.length > 0) { params.set("status", statusFilter.join(",")); } if (debouncedSearch) { params.set("search", debouncedSearch); } const data = await apiJson(`/api/jobs/table?${params}`); setJobs((prev) => data.map((serverJob) => { const local = prev.find((j) => j.id === serverJob.id); const terminal = ["completed", "skipped", "failed", "cancelled"]; const serverIsTerminal = terminal.includes(serverJob.status); if ( local && terminal.includes(local.status) && serverIsTerminal ) { // Both agree this is terminal — keep // local status to prevent SSE→poll flicker. return { ...serverJob, status: local.status }; } // Server says it changed (e.g. retry queued it) // — trust the server. return serverJob; }) ); setActionError(null); } catch (e) { const message = isApiError(e) ? e.message : "Failed to fetch jobs"; setActionError(message); if (!silent) { showToast({ kind: "error", title: "Jobs", message }); } } finally { setLoading(false); if (!silent) { setRefreshing(false); } } }, [activeTab, debouncedSearch, page, sortBy, sortDesc]); const fetchJobsRef = useRef<() => Promise>(async () => undefined); useEffect(() => { fetchJobsRef.current = async () => { await fetchJobs(true); }; }, [fetchJobs]); useEffect(() => { void fetchJobs(false); }, [fetchJobs]); useEffect(() => { const pollVisible = () => { if (document.visibilityState === "visible") { void fetchJobsRef.current(); } }; const interval = window.setInterval(pollVisible, 5000); document.addEventListener("visibilitychange", pollVisible); return () => { window.clearInterval(interval); document.removeEventListener("visibilitychange", pollVisible); }; }, []); useEffect(() => { let eventSource: EventSource | null = null; let cancelled = false; let reconnectTimeout: number | null = null; let reconnectAttempts = 0; const getReconnectDelay = () => { // Exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s const baseDelay = 1000; const maxDelay = 30000; const delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts), maxDelay); // Add jitter (±25%) to prevent thundering herd const jitter = delay * 0.25 * (Math.random() * 2 - 1); return Math.round(delay + jitter); }; const connect = () => { if (cancelled) return; eventSource?.close(); eventSource = new EventSource("/api/events"); eventSource.onopen = () => { // Reset reconnect attempts on successful connection reconnectAttempts = 0; }; eventSource.addEventListener("status", (e) => { try { const { job_id, status } = JSON.parse(e.data) as { job_id: number; status: string; }; if (status === "encoding") { encodeStartTimes.current.set(job_id, Date.now()); } else { encodeStartTimes.current.delete(job_id); } setJobs((prev) => prev.map((job) => job.id === job_id ? { ...job, status } : job ) ); } catch { /* ignore malformed */ } }); eventSource.addEventListener("progress", (e) => { try { const { job_id, percentage } = JSON.parse(e.data) as { job_id: number; percentage: number; }; setJobs((prev) => prev.map((job) => job.id === job_id ? { ...job, progress: percentage } : job ) ); } catch { /* ignore malformed */ } }); eventSource.addEventListener("decision", () => { // Re-fetch full job list when decisions are made void fetchJobsRef.current(); }); eventSource.onerror = () => { eventSource?.close(); if (!cancelled) { reconnectAttempts++; const delay = getReconnectDelay(); reconnectTimeout = window.setTimeout(connect, delay); } }; }; connect(); return () => { cancelled = true; eventSource?.close(); if (reconnectTimeout !== null) { window.clearTimeout(reconnectTimeout); } }; }, []); useEffect(() => { const encodingJobIds = new Set(); const now = Date.now(); for (const job of jobs) { if (job.status !== "encoding") { continue; } encodingJobIds.add(job.id); if (!encodeStartTimes.current.has(job.id)) { encodeStartTimes.current.set(job.id, now); } } for (const jobId of Array.from(encodeStartTimes.current.keys())) { if (!encodingJobIds.has(jobId)) { encodeStartTimes.current.delete(jobId); } } }, [jobs]); useEffect(() => { if (!menuJobId) return; const handleClick = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setMenuJobId(null); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [menuJobId]); useEffect(() => { confirmOpenRef.current = confirmState !== null; }, [confirmState]); useEffect(() => { if (!focusedJob) { return; } detailLastFocusedRef.current = document.activeElement as HTMLElement | null; const root = detailDialogRef.current; if (root) { const focusables = focusableElements(root); if (focusables.length > 0) { focusables[0].focus(); } else { root.focus(); } } const onKeyDown = (event: KeyboardEvent) => { if (!focusedJob || confirmOpenRef.current) { return; } if (event.key === "Escape") { event.preventDefault(); setFocusedJob(null); return; } if (event.key !== "Tab") { return; } const dialogRoot = detailDialogRef.current; if (!dialogRoot) { return; } const focusables = focusableElements(dialogRoot); if (focusables.length === 0) { event.preventDefault(); dialogRoot.focus(); return; } const first = focusables[0]; const last = focusables[focusables.length - 1]; const current = document.activeElement as HTMLElement | null; if (event.shiftKey && current === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && current === last) { event.preventDefault(); first.focus(); } }; document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("keydown", onKeyDown); if (detailLastFocusedRef.current) { detailLastFocusedRef.current.focus(); } }; }, [focusedJob]); const toggleSelect = (id: number) => { const newSet = new Set(selected); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); setSelected(newSet); }; const toggleSelectAll = () => { if (selected.size === jobs.length && jobs.length > 0) { setSelected(new Set()); } else { setSelected(new Set(jobs.map(j => j.id))); } }; const selectedJobs = jobs.filter((job) => selected.has(job.id)); const hasSelectedActiveJobs = selectedJobs.some(isJobActive); const activeCount = jobs.filter((job) => isJobActive(job)).length; const failedCount = jobs.filter((job) => ["failed", "cancelled"].includes(job.status)).length; const completedCount = jobs.filter((job) => job.status === "completed").length; const handleBatch = async (action: "cancel" | "restart" | "delete") => { if (selected.size === 0) return; setActionError(null); try { await apiAction("/api/jobs/batch", { method: "POST", body: JSON.stringify({ action, ids: Array.from(selected) }) }); setSelected(new Set()); showToast({ kind: "success", title: "Jobs", message: `${action[0].toUpperCase()}${action.slice(1)} request sent for selected jobs.`, }); await fetchJobs(); } catch (e) { const message = formatJobActionError(e, "Batch action failed"); setActionError(message); showToast({ kind: "error", title: "Jobs", message }); } }; const clearCompleted = async () => { setActionError(null); try { const result = await apiJson("/api/jobs/clear-completed", { method: "POST", }); showToast({ kind: "success", title: "Jobs", message: result.message }); if (activeTab === "completed" && result.count > 0) { showToast({ kind: "info", title: "Jobs", message: "Completed jobs archived. View them in the Archived tab.", }); } await fetchJobs(); } catch (e) { const message = isApiError(e) ? e.message : "Failed to clear completed jobs"; setActionError(message); showToast({ kind: "error", title: "Jobs", message }); } }; const fetchJobDetails = async (id: number) => { setActionError(null); setDetailLoading(true); try { const data = await apiJson(`/api/jobs/${id}/details`); setFocusedJob(data); } catch (e) { const message = isApiError(e) ? e.message : "Failed to fetch job details"; setActionError(message); showToast({ kind: "error", title: "Jobs", message }); } finally { setDetailLoading(false); } }; const handleAction = async (id: number, action: "cancel" | "restart" | "delete") => { setActionError(null); try { await apiAction(`/api/jobs/${id}/${action}`, { method: "POST" }); if (action === "delete") { setFocusedJob((current) => (current?.job.id === id ? null : current)); } else if (focusedJob?.job.id === id) { await fetchJobDetails(id); } await fetchJobs(); showToast({ kind: "success", title: "Jobs", message: `Job ${action} request completed.`, }); } catch (e) { const message = formatJobActionError(e, `Job ${action} failed`); setActionError(message); showToast({ kind: "error", title: "Jobs", message }); } }; const handlePriority = async (job: Job, priority: number, label: string) => { setActionError(null); try { await apiAction(`/api/jobs/${job.id}/priority`, { method: "POST", body: JSON.stringify({ priority }), }); if (focusedJob?.job.id === job.id) { setFocusedJob({ ...focusedJob, job: { ...focusedJob.job, priority, }, }); } await fetchJobs(); showToast({ kind: "success", title: "Jobs", message: `${label} for job #${job.id}.` }); } catch (e) { const message = formatJobActionError(e, "Failed to update priority"); setActionError(message); showToast({ kind: "error", title: "Jobs", message }); } }; const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const formatDuration = (seconds: number) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return [h, m, s].map(v => v.toString().padStart(2, "0")).join(":"); }; const calcEta = (jobId: number, progress: number): string | null => { if (progress <= 0 || progress >= 100) { return null; } const startMs = encodeStartTimes.current.get(jobId); if (!startMs) { return null; } const elapsedMs = Date.now() - startMs; const totalMs = elapsedMs / (progress / 100); const remainingMs = totalMs - elapsedMs; const remainingSecs = Math.round(remainingMs / 1000); if (remainingSecs < 0) { return null; } if (remainingSecs < 60) { return `~${remainingSecs}s remaining`; } const mins = Math.ceil(remainingSecs / 60); return `~${mins} min remaining`; }; const getStatusBadge = (status: string) => { const styles: Record = { queued: "bg-helios-slate/10 text-helios-slate border-helios-slate/20", analyzing: "bg-blue-500/10 text-blue-500 border-blue-500/20", encoding: "bg-helios-solar/10 text-helios-solar border-helios-solar/20 animate-pulse", remuxing: "bg-helios-solar/10 text-helios-solar border-helios-solar/20 animate-pulse", completed: "bg-green-500/10 text-green-500 border-green-500/20", failed: "bg-red-500/10 text-red-500 border-red-500/20", cancelled: "bg-red-500/10 text-red-500 border-red-500/20", skipped: "bg-gray-500/10 text-gray-500 border-gray-500/20", archived: "bg-zinc-500/10 text-zinc-400 border-zinc-500/20", resuming: "bg-helios-solar/10 text-helios-solar border-helios-solar/20 animate-pulse", }; return ( {status} ); }; const openConfirm = (config: { title: string; body: string; confirmLabel: string; confirmTone?: "danger" | "primary"; onConfirm: () => Promise | void; }) => { setConfirmState(config); }; const focusedDecision = focusedJob?.job.decision_reason ? humanizeSkipReason(focusedJob.job.decision_reason) : null; const focusedJobLogs = focusedJob?.job_logs ?? []; const shouldShowFfmpegOutput = focusedJob ? ["failed", "completed", "skipped"].includes(focusedJob.job.status) && focusedJobLogs.length > 0 : false; const completedEncodeStats = focusedJob?.job.status === "completed" ? focusedJob.encode_stats : null; return (
{activeCount} {" "}active {failedCount} {" "}failed {completedCount} {" "}completed
{/* Toolbar */}
{(["all", "active", "queued", "completed", "failed", "skipped", "archived"] as TabType[]).map((tab) => ( ))}
setSearchInput(e.target.value)} className="w-full bg-helios-surface border border-helios-line/20 rounded-lg pl-9 pr-4 py-2 text-sm text-helios-ink focus:border-helios-solar outline-none" />
{actionError && (
{actionError}
)} {/* Batch Actions Bar */} {selected.size > 0 && (
{selected.size} jobs selected {hasSelectedActiveJobs && (

Active jobs must be cancelled before they can be restarted or deleted.

)}
)} {/* Table */}
{loading && jobs.length === 0 ? ( Array.from({ length: 5 }).map((_, index) => ( )) ) : jobs.length === 0 ? ( ) : ( jobs.map((job) => ( void fetchJobDetails(job.id)} className={cn( "group hover:bg-helios-surface/80 transition-all cursor-pointer", selected.has(job.id) && "bg-helios-surface-soft", focusedJob?.job.id === job.id && "bg-helios-solar/5" )} > )) )}
0 && jobs.every(j => selected.has(j.id))} onChange={toggleSelectAll} className="rounded border-helios-line/30 bg-helios-surface-soft accent-helios-solar" /> File Status Progress Updated
No jobs found
e.stopPropagation()}> toggleSelect(job.id)} className="rounded border-helios-line/30 bg-helios-surface-soft accent-helios-solar" /> {job.input_path.split(/[/\\]/).pop()}
{job.input_path} P{job.priority}
{getStatusBadge(job.status)} {job.status === "failed" && (() => { // Reference tick so React re-renders countdowns on interval void tick; const countdown = retryCountdown(job); return countdown ? (

{countdown}

) : null; })()}
{["encoding", "analyzing", "remuxing"].includes(job.status) ? (
{job.progress.toFixed(1)}%
{job.status === "encoding" && (() => { const eta = calcEta(job.id, job.progress); return eta ? (

{eta}

) : null; })()} {job.status === "encoding" && job.encoder && ( {job.encoder} )}
) : ( job.vmaf_score ? ( VMAF: {job.vmaf_score.toFixed(1)} ) : ( - ) )}
{new Date(job.updated_at).toLocaleString()} e.stopPropagation()}>
{menuJobId === job.id && ( {(job.status === "failed" || job.status === "cancelled") && ( )} {["encoding", "analyzing", "remuxing"].includes(job.status) && ( )} {!isJobActive(job) && ( )} )}
{/* Footer Actions */}

Showing {jobs.length} jobs (Limit 50)

{/* Detail Overlay - rendered via portal to escape layout constraints */} {typeof document !== "undefined" && createPortal( {focusedJob && ( <> setFocusedJob(null)} className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]" />
{/* Header */}
{getStatusBadge(focusedJob.job.status)} Job ID #{focusedJob.job.id} Priority {focusedJob.job.priority}

{focusedJob.job.input_path.split(/[/\\]/).pop()}

{focusedJob.job.input_path}

{detailLoading && (

Loading job details...

)} {focusedJob.metadata || completedEncodeStats ? ( <> {focusedJob.metadata && ( <> {/* Stats Grid */}
Video Codec

{focusedJob.metadata.codec_name || "Unknown"}

{(focusedJob.metadata.bit_depth ? `${focusedJob.metadata.bit_depth}-bit` : "Unknown bit depth")} • {focusedJob.metadata.container.toUpperCase()}

Resolution

{`${focusedJob.metadata.width}x${focusedJob.metadata.height}`}

{focusedJob.metadata.fps.toFixed(2)} FPS

Duration

{formatDuration(focusedJob.metadata.duration_secs)}

{/* Media Details */}

Input Details

File Size {formatBytes(focusedJob.metadata.size_bytes)}
Video Bitrate {(focusedJob.metadata.video_bitrate_bps ?? focusedJob.metadata.container_bitrate_bps) ? `${(((focusedJob.metadata.video_bitrate_bps ?? focusedJob.metadata.container_bitrate_bps) as number) / 1000).toFixed(0)} kbps` : "-"}
Audio {focusedJob.metadata.audio_codec || "N/A"} ({focusedJob.metadata.audio_channels || 0}ch)

Output Details

{focusedJob.encode_stats ? (
Result Size {formatBytes(focusedJob.encode_stats.output_size_bytes)}
Reduction {((1 - focusedJob.encode_stats.compression_ratio) * 100).toFixed(1)}% Saved
VMAF Score
{focusedJob.encode_stats.vmaf_score?.toFixed(1) || "-"}
) : (
{focusedJob.job.status === "encoding" ? "Encoding in progress..." : focusedJob.job.status === "remuxing" ? "Remuxing in progress..." : "No encode data available"}
)}
)} {completedEncodeStats && (

Encode Results

Input size {formatBytes(completedEncodeStats.input_size_bytes)}
Output size {formatBytes(completedEncodeStats.output_size_bytes)}
Reduction {completedEncodeStats.input_size_bytes > 0 ? `${((1 - completedEncodeStats.output_size_bytes / completedEncodeStats.input_size_bytes) * 100).toFixed(1)}% saved` : "—"}
Encode time {formatDuration(completedEncodeStats.encode_time_seconds)}
Speed {`${completedEncodeStats.encode_speed.toFixed(2)}\u00d7 realtime`}
Avg bitrate {`${completedEncodeStats.avg_bitrate_kbps} kbps`}
VMAF {completedEncodeStats.vmaf_score?.toFixed(1) ?? "—"}
)} ) : (

Waiting for analysis

Metadata will appear once this job is picked up by the engine.

)} {/* Decision Info */} {focusedJob.job.decision_reason && focusedJob.job.status !== "failed" && focusedJob.job.status !== "skipped" && (
Decision Context
{focusedDecision && (

{focusedJob.job.status === "completed" ? "Transcoded" : focusedDecision.summary}

{focusedDecision.detail}

{Object.keys(focusedDecision.measured).length > 0 && (
{Object.entries(focusedDecision.measured).map(([k, v]) => (
{k} {v}
))}
)} {focusedDecision.action && (
{focusedDecision.action}
)}
)}
)} {focusedJob.job.status === "skipped" && focusedJob.job.decision_reason && (

Alchemist analysed this file and decided not to transcode it. Here's why:

{focusedDecision && (

{focusedDecision.summary}

{focusedDecision.detail}

{Object.keys(focusedDecision.measured).length > 0 && (
{Object.entries(focusedDecision.measured).map(([k, v]) => (
{k} {v}
))}
)} {focusedDecision.action && (
{focusedDecision.action}
)}
)}
)} {focusedJob.job.status === "failed" && (
Failure Reason
{focusedJob.job_failure_summary ? ( <>

{explainFailureSummary(focusedJob.job_failure_summary)}

{focusedJob.job_failure_summary}

) : (

No error details captured. Check the logs below.

)}
)} {shouldShowFfmpegOutput && (
Show FFmpeg output ({focusedJobLogs.length} lines)
{focusedJobLogs.map((entry) => (
{entry.message}
))}
)} {/* Action Toolbar */}
{(focusedJob.job.status === 'failed' || focusedJob.job.status === 'cancelled') && ( )} {["encoding", "analyzing", "remuxing"].includes(focusedJob.job.status) && ( )}
{!isJobActive(focusedJob.job) && ( )}
)} , document.body )} setConfirmState(null)} onConfirm={async () => { if (!confirmState) { return; } await confirmState.onConfirm(); }} />
); } export default withErrorBoundary(JobManager, "Job Management");