import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card } from 'primereact/card'; import { Button } from 'primereact/button'; import { InputText } from 'primereact/inputtext'; import { Tag } from 'primereact/tag'; import { Paginator } from 'primereact/paginator'; import { api } from '../api/client'; import { classifyJob } from '../utils/jobTaxonomy'; import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation'; const ACTIVE_STATUSES = [ 'ANALYZING', 'METADATA_SELECTION', 'WAITING_FOR_USER_DECISION', 'READY_TO_START', 'MEDIAINFO_CHECK', 'READY_TO_ENCODE', 'RIPPING', 'ENCODING', 'CD_METADATA_SELECTION', 'CD_READY_TO_RIP', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING' ]; const STATUS_FILTERS = [ { key: 'all', label: 'Alle' }, { key: 'active', label: 'Aktiv' }, { key: 'errors', label: 'Fehler/Abbruch' } ]; const VIEW_FILTERS = [ { key: 'all', label: 'Alle Jobs' }, { key: 'ripper', label: 'Ripper-View' }, { key: 'converter', label: 'Converter-View' }, { key: 'audiobook', label: 'Audiobooks' } ]; function normalizeJobId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function formatUpdatedAt(value) { if (!value) { return '-'; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return '-'; } return parsed.toLocaleString('de-DE'); } function getKindLabel(meta) { if (meta.family === 'converter') { if (meta.converterMediaType === 'audio') { return 'Converter Audio'; } if (meta.converterMediaType === 'iso') { return 'Converter ISO'; } return 'Converter Video'; } if (meta.family === 'audiobook') { return 'Audiobook'; } if (meta.mediaType === 'bluray') { return 'Blu-ray'; } if (meta.mediaType === 'dvd') { return 'DVD'; } if (meta.mediaType === 'cd') { return 'CD'; } return 'Sonstiges'; } function getKindSeverity(meta) { if (meta.family === 'converter') { return 'info'; } if (meta.family === 'audiobook') { return 'warning'; } if (meta.mediaType === 'bluray' || meta.mediaType === 'dvd') { return 'success'; } return 'secondary'; } export default function JobsInboxPage() { const navigate = useNavigate(); const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(false); const [viewFilter, setViewFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [search, setSearch] = useState(''); const [queuedJobIds, setQueuedJobIds] = useState(new Set()); const [lastUpdatedAt, setLastUpdatedAt] = useState(null); const [first, setFirst] = useState(0); const [rows, setRows] = useState(25); const loadJobs = useCallback(async () => { setLoading(true); try { const query = { limit: 500, lite: true }; if (statusFilter === 'active') { query.statuses = ACTIVE_STATUSES; } else if (statusFilter === 'errors') { query.statuses = ['ERROR', 'CANCELLED']; } const [jobsResponse, queueResponse] = await Promise.allSettled([ api.getJobs(query), api.getPipelineQueue() ]); const nextJobs = jobsResponse.status === 'fulfilled' && Array.isArray(jobsResponse.value?.jobs) ? jobsResponse.value.jobs : []; setJobs(nextJobs); if (queueResponse.status === 'fulfilled') { const rows = Array.isArray(queueResponse.value?.queue?.queuedJobs) ? queueResponse.value.queue.queuedJobs : []; setQueuedJobIds(new Set(rows.map((item) => normalizeJobId(item?.jobId)).filter(Boolean))); } else { setQueuedJobIds(new Set()); } setLastUpdatedAt(new Date().toISOString()); } finally { setLoading(false); } }, [statusFilter]); useEffect(() => { loadJobs(); const interval = setInterval(() => { loadJobs().catch(() => null); }, 7000); return () => clearInterval(interval); }, [loadJobs]); const jobsWithMeta = useMemo(() => ( jobs.map((job) => ({ job, meta: classifyJob(job) })) ), [jobs]); const viewCounts = useMemo(() => { let all = 0; let ripper = 0; let converter = 0; let audiobook = 0; for (const item of jobsWithMeta) { all += 1; if (item.meta.family === 'converter') { converter += 1; } else { ripper += 1; } if (item.meta.family === 'audiobook') { audiobook += 1; } } return { all, ripper, converter, audiobook }; }, [jobsWithMeta]); const filteredRows = useMemo(() => { const normalizedSearch = String(search || '').trim().toLowerCase(); return jobsWithMeta.filter(({ job, meta }) => { if (viewFilter === 'ripper' && meta.family === 'converter') { return false; } if (viewFilter === 'converter' && meta.family !== 'converter') { return false; } if (viewFilter === 'audiobook' && meta.family !== 'audiobook') { return false; } if (!normalizedSearch) { return true; } const haystack = [ job?.title, job?.detected_title, job?.imdb_id, job?.status, job?.job_kind, job?.media_type ] .map((value) => String(value || '').trim().toLowerCase()) .filter(Boolean) .join(' '); return haystack.includes(normalizedSearch); }); }, [jobsWithMeta, search, viewFilter]); useEffect(() => { setFirst(0); }, [viewFilter, statusFilter, search]); useEffect(() => { if (filteredRows.length === 0) { if (first !== 0) { setFirst(0); } return; } if (first >= filteredRows.length) { const pageStart = Math.max(0, Math.floor((filteredRows.length - 1) / rows) * rows); setFirst(pageStart); } }, [filteredRows.length, first, rows]); const pagedRows = useMemo( () => filteredRows.slice(first, first + rows), [filteredRows, first, rows] ); return (
{VIEW_FILTERS.map((filter) => { const isActive = filter.key === viewFilter; const count = viewCounts[filter.key] ?? 0; return (
{STATUS_FILTERS.map((filter) => { const isActive = filter.key === statusFilter; return (
setSearch(event.target.value)} placeholder="Suche nach Titel, Status oder IMDB-ID" className="job-inbox-search-input" /> Letztes Update: {formatUpdatedAt(lastUpdatedAt)} | Treffer: {filteredRows.length}
{loading && filteredRows.length === 0 ? (

Inbox wird geladen ...

) : filteredRows.length === 0 ? (

Keine Jobs für den aktuellen Filter gefunden.

) : ( pagedRows.map(({ job, meta }) => { const jobId = normalizeJobId(job?.id); if (!jobId) { return null; } const isQueued = queuedJobIds.has(jobId); const normalizedStatus = normalizeStatus(job?.status); const targetPath = meta.family === 'converter' ? '/converter' : '/ripper'; const jobTitle = String(job?.title || job?.detected_title || '').trim() || `Job #${jobId}`; return (
#{jobId} | {jobTitle} Status: {getStatusLabel(normalizedStatus, { queued: isQueued })} {' | '} Aktualisiert: {formatUpdatedAt(job?.updated_at || job?.created_at || null)}
); }) )}
{filteredRows.length > 0 ? (
{ setFirst(event.first); setRows(event.rows); }} template="PrevPageLink PageLinks NextPageLink RowsPerPageDropdown CurrentPageReport" currentPageReportTemplate="{first} - {last} von {totalRecords}" />
) : null}
); }