document.addEventListener('DOMContentLoaded', function () { var state = { items: [], workspaceId: null, loading: false, error: '', openId: null, }; var list = document.getElementById('async-jobs-list'); var summary = document.getElementById('async-jobs-summary'); var refreshButton = document.getElementById('async-jobs-refresh'); var statusFilter = document.getElementById('async-jobs-status'); function tKey(key, vars) { if (!window.t) return key; return t(key, vars); } function currentWorkspaceId() { var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; return workspace ? workspace.id : null; } function formatJson(value) { if (value === null || value === undefined) return ''; if (typeof value === 'string') return value; return JSON.stringify(value, null, 2); } function formatDateTime(value) { if (!value) return '—'; return new Date(value).toLocaleString(); } function element(tag, className, text) { var node = document.createElement(tag); if (className) node.className = className; if (text !== undefined && text !== null) node.textContent = text; return node; } function renderEmpty(title, body) { list.innerHTML = ''; var empty = element('div', 'empty-state'); empty.appendChild(element('div', 'empty-state-title', title)); empty.appendChild(element('div', 'empty-state-text', body)); list.appendChild(empty); } function appendMetaItem(parent, label, value) { var item = element('div', 'resource-meta-item'); item.appendChild(element('div', 'resource-meta-label', label)); item.appendChild(element('div', 'resource-meta-value', value)); parent.appendChild(item); } function appendDetailBlock(parent, title, value, dataResultFor) { var block = element('div', 'resource-detail-block'); block.appendChild(element('div', 'resource-detail-title', title)); var pre = element('pre', 'resource-detail-pre', value); if (dataResultFor) pre.setAttribute('data-result-for', dataResultFor); block.appendChild(pre); parent.appendChild(block); } function renderJobCard(item) { var open = state.openId === item.id; var card = element('div', 'resource-card'); card.setAttribute('data-job-id', item.id); var header = element('div', 'resource-card-header'); var headerMain = element('div'); headerMain.appendChild(element('div', 'resource-card-title', item.id)); headerMain.appendChild(element('div', 'resource-card-subtitle', tKey('async_jobs.operation', { operation: item.operation_id }))); var pillRow = element('div', 'resource-pill-row'); pillRow.appendChild( element('span', 'resource-status-pill ' + String(item.status || '').toLowerCase(), tKey('async_jobs.status.' + item.status)) ); headerMain.appendChild(pillRow); header.appendChild(headerMain); var actions = element('div', 'resource-card-actions'); if (item.status === 'created' || item.status === 'running') { var cancelButton = element('button', 'btn-secondary', tKey('async_jobs.cancel')); cancelButton.setAttribute('data-action', 'cancel'); cancelButton.setAttribute('data-job-id', item.id); actions.appendChild(cancelButton); } var toggleButton = element( 'button', 'btn-secondary', open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details') ); toggleButton.setAttribute('data-action', 'toggle'); toggleButton.setAttribute('data-job-id', item.id); actions.appendChild(toggleButton); header.appendChild(actions); card.appendChild(header); var metaGrid = element('div', 'resource-meta-grid'); appendMetaItem(metaGrid, tKey('async_jobs.meta.created'), formatDateTime(item.created_at)); appendMetaItem(metaGrid, tKey('async_jobs.meta.updated'), formatDateTime(item.updated_at)); appendMetaItem(metaGrid, tKey('async_jobs.meta.finished'), formatDateTime(item.finished_at)); appendMetaItem(metaGrid, tKey('async_jobs.meta.expires'), formatDateTime(item.expires_at)); card.appendChild(metaGrid); if (open) { appendDetailBlock(card, tKey('async_jobs.progress'), formatJson(item.progress)); appendDetailBlock(card, tKey('async_jobs.error_preview'), formatJson(item.error)); if ( item.status === 'completed' || item.status === 'failed' || item.status === 'cancelled' || item.status === 'expired' ) { appendDetailBlock(card, tKey('async_jobs.result'), tKey('async_jobs.result_loading'), item.id); } } return card; } function render() { summary.textContent = tKey('async_jobs.summary', { count: state.items.length }); if (state.loading && state.items.length === 0) { renderEmpty(tKey('async_jobs.loading.title'), tKey('async_jobs.loading.body')); return; } if (state.error) { renderEmpty(tKey('async_jobs.error.title'), state.error); return; } if (!state.items.length) { renderEmpty(tKey('async_jobs.empty.title'), tKey('async_jobs.empty.body')); return; } list.innerHTML = ''; state.items.forEach(function(item) { list.appendChild(renderJobCard(item)); }); list.querySelectorAll('[data-action="toggle"]').forEach(function(button) { button.addEventListener('click', function () { var jobId = this.getAttribute('data-job-id'); state.openId = state.openId === jobId ? null : jobId; if (state.openId) { void loadDetail(jobId); } else { render(); } }); }); list.querySelectorAll('[data-action="cancel"]').forEach(function(button) { button.addEventListener('click', async function () { try { var jobId = this.getAttribute('data-job-id'); await window.CrankApi.cancelAsyncJob(state.workspaceId, jobId); if (window.CrankUi) { window.CrankUi.success(tKey('async_jobs.toast.cancel.body'), tKey('async_jobs.toast.cancel.title')); } await load(); } catch (error) { if (window.CrankUi) { window.CrankUi.error(error.message || tKey('async_jobs.toast.cancel_error.body'), tKey('async_jobs.toast.cancel_error.title')); } } }); }); } async function loadResult(jobId) { try { var result = await window.CrankApi.getAsyncJobResult(state.workspaceId, jobId); var node = document.querySelector('[data-result-for="' + jobId + '"]'); if (node) node.textContent = formatJson(result); } catch (error) { var failedNode = document.querySelector('[data-result-for="' + jobId + '"]'); if (failedNode) failedNode.textContent = error.message || tKey('async_jobs.result_error'); } } async function loadDetail(jobId) { var detail = await window.CrankApi.getAsyncJob(state.workspaceId, jobId); state.items = state.items.map(function(item) { return item.id === jobId ? detail : item; }); render(); if (detail.status === 'completed' || detail.status === 'failed' || detail.status === 'cancelled' || detail.status === 'expired') { await loadResult(jobId); } } async function load() { state.workspaceId = currentWorkspaceId(); if (!state.workspaceId) { state.items = []; state.error = tKey('async_jobs.error.workspace'); render(); return; } state.loading = true; state.error = ''; render(); try { var response = await window.CrankApi.listAsyncJobs(state.workspaceId, { status: statusFilter.value || null, page: 1, page_size: 50, }); state.items = response.items || []; } catch (error) { state.error = error.message || tKey('async_jobs.error.load'); } finally { state.loading = false; render(); } } refreshButton.addEventListener('click', load); statusFilter.addEventListener('change', load); document.addEventListener('workspace:changed', load); void load(); });