114 lines
4.8 KiB
JavaScript
114 lines
4.8 KiB
JavaScript
// ── 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);
|
|
})();
|