// ── Background job history ─────────────────────────────────────────────── (function initBackgroundJobs() { const list = document.getElementById('background-jobs-list'); const refresh = document.getElementById('background-jobs-refresh'); if (!list || !refresh) return; const labels = { transcription: 'Transcription', voice_design: 'Voice design', casting: 'Voice casting', consistent_images: 'Consistent images' }; let loading = false; function node(value, fallback = '—') { const item = document.createElement('span'); item.textContent = value == null || value === '' ? fallback : String(value); return item; } function statusClass(job) { if (job.cancelled || job.stage === 'cancelled') return 'is-cancelled'; if (job.error || job.stage === 'error' || job.stage === 'failed') return 'is-error'; if (job.done) return 'is-done'; return 'is-active'; } function statusLabel(job) { if (job.cancelled || job.stage === 'cancelled') return 'Cancelled'; if (job.error || job.stage === 'error' || job.stage === 'failed') return 'Failed'; if (job.done) return 'Completed'; return job.stage === 'queued' ? 'Queued' : 'Running'; } function cancelEndpoint(kind, id) { const paths = { transcription: '/api/transcribe/cancel/', voice_design: '/api/voice-design/cancel/', casting: '/api/match-characters-voices/cancel/', consistent_images: '/api/character-generate-consistent-images/cancel/' }; return paths[kind] ? paths[kind] + encodeURIComponent(id) : ''; } function render(jobs) { list.replaceChildren(); if (!jobs.length) { const empty = node('No background jobs yet. Start a transcription, voice design, cast, or image workflow to see it here.'); empty.className = 'perf-history-empty'; list.appendChild(empty); return; } jobs.forEach(job => { const row = document.createElement('div'); row.className = 'background-job-row ' + statusClass(job); const main = document.createElement('div'); main.className = 'background-job-main'; const title = document.createElement('strong'); title.textContent = labels[job.kind] || job.kind || 'Background job'; main.appendChild(title); const id = document.createElement('code'); id.textContent = String(job.job_id || '').slice(0, 12); main.appendChild(id); const detail = node(job.error || job.detail || job.stage); detail.className = 'background-job-detail'; main.appendChild(detail); const meta = node(`${statusLabel(job)} · ${Number(job.elapsed || 0).toFixed(1)}s`); meta.className = 'background-job-meta'; main.appendChild(meta); row.appendChild(main); const actions = document.createElement('div'); actions.className = 'background-job-actions'; if (!job.done && !job.cancelled && job.job_id) { const endpoint = cancelEndpoint(job.kind, job.job_id); if (endpoint) { const button = document.createElement('button'); button.type = 'button'; button.className = 'btn-secondary btn-sm'; button.textContent = 'Cancel'; button.addEventListener('click', async () => { button.disabled = true; try { const response = await fetch(endpoint, { method: 'POST' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); await load(); } catch (error) { button.disabled = false; if (typeof toast === 'function') toast('Could not cancel job: ' + error.message, 'error'); } }); actions.appendChild(button); } } const note = node('Reopen source to retry'); note.className = 'background-job-retry-note'; note.title = 'Inputs are not retained for replay, by design.'; actions.appendChild(note); row.appendChild(actions); list.appendChild(row); }); } async function load() { if (loading) return; loading = true; refresh.disabled = true; try { const response = await fetch('/api/jobs?limit=50', { cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); render(Array.isArray(payload.jobs) ? payload.jobs : []); } catch (error) { list.replaceChildren(); const empty = node('Background job history unavailable: ' + error.message); empty.className = 'perf-history-empty'; list.appendChild(empty); } finally { loading = false; refresh.disabled = false; } } refresh.addEventListener('click', load); load(); setInterval(() => { if (document.getElementById('s-performance')?.classList.contains('is-active')) load(); }, 10000); })();