2 lines
6.3 KiB
JavaScript
2 lines
6.3 KiB
JavaScript
export const jobToolsSource = " const JOB_DETAILS_STORAGE_KEY = 'pytorrent.jobs.showDetails.v1';\n\n function readJobDetailsPreference(){\n try{ return localStorage.getItem(JOB_DETAILS_STORAGE_KEY) === '1'; }\n catch(e){ return false; }\n }\n\n function saveJobDetailsPreference(showDetails){\n try{ localStorage.setItem(JOB_DETAILS_STORAGE_KEY, showDetails ? '1' : '0'); }\n catch(e){}\n }\n\n function syncJobDetailsControl(){\n if($('jobsShowDetails')) $('jobsShowDetails').checked = readJobDetailsPreference();\n }\n\n function jobActions(r){ const id=esc(r.id); const status=String(r.status||''); const actions=[]; if(status==='failed'||status==='cancelled') actions.push(`<button class=\"btn btn-xs btn-outline-primary job-retry\" data-id=\"${id}\"><i class=\"fa-solid fa-rotate-left\"></i> retry</button>`); if(status==='pending') actions.push(`<button class=\"btn btn-xs btn-outline-warning job-force\" data-id=\"${id}\" title=\"Run now in a separate worker\"><i class=\"fa-solid fa-bolt\"></i> force</button>`); if(status==='pending'||status==='running') actions.push(`<button class=\"btn btn-xs btn-outline-danger job-cancel\" data-id=\"${id}\" data-status=\"${esc(status)}\"><i class=\"fa-solid fa-triangle-exclamation\"></i> emergency cancel</button>`); return actions.join(' ') || '<span class=\"text-muted\">-</span>'; }\n function jobStatusBadgeClass(status){\n // Note: Running means active work, so it uses primary instead of danger; danger stays reserved for failed.\n const classes={done:'success',failed:'danger',running:'primary',cancelled:'secondary',pending:'warning'};\n return classes[String(status||'')] || 'warning';\n }\n async function loadJobs(page=jobsPage){\n const box=$('jobsTable');\n // Note: Finished shows only a real finished_at value; running/pending do not receive a date from updated_at.\n if(!box) return;\n jobsPage=Math.max(0,page|0);\n syncJobDetailsControl();\n box.innerHTML='<span class=\"spinner-border spinner-border-sm\"></span> Loading jobs...';\n const offset=jobsPage*jobsLimit;\n const j=await (await fetch(`/api/jobs?limit=${jobsLimit}&offset=${offset}`, {cache:'no-store'})).json();\n const rows=j.jobs||[];\n jobsTotal=Number(j.total||rows.length);\n const showDetails = readJobDetailsPreference();\n const details=r=>{\n const count=Number(r.hash_count||0);\n if(!showDetails) return '<span class=\"text-muted\">Details hidden</span>';\n const bits=[];\n if(r.is_bulk || count>1) bits.push('<span class=\"badge text-bg-info\">bulk</span>');\n if(count) bits.push(`${esc(count)} torrent${count === 1 ? '' : 's'}`);\n if(r.summary) bits.push(esc(r.summary));\n return bits.join('<br>') || '-';\n };\n box.innerHTML=responsiveTable(\n ['Status','Source','Action','Profile','Count','Details','Attempts','Started','Finished','Error','Actions'],\n rows.map(r=>[\n `<span class=\"badge text-bg-${jobStatusBadgeClass(r.status)}\">${esc(r.status)}</span>`,\n r.source==='automation'?`<span class=\"badge text-bg-info\" title=\"${esc(r.source_label||'automation')}\"><i class=\"fa-solid fa-wand-magic-sparkles\"></i> automation</span>`:(r.is_forced?'<span class=\"badge text-bg-warning\"><i class=\"fa-solid fa-bolt\"></i> forced</span>':'<span class=\"badge text-bg-secondary\">user</span>'),\n esc(r.action),\n esc(r.profile_id),\n esc(r.hash_count||0),\n details(r),\n esc(r.attempts||0),\n humanDateCell(r.started_at||r.created_at),\n humanDateCell(r.finished_at),\n compactCell(r.error||'',140),\n jobActions(r),\n ]),\n 'jobs-table'\n );\n renderJobsPager();\n }\n function renderJobsPager(){ const p=$('jobsPager'); if(!p)return; const pages=Math.max(1,Math.ceil(jobsTotal/jobsLimit)); p.innerHTML=`<div class=\"d-flex align-items-center gap-2 flex-wrap\"><button class=\"btn btn-sm btn-outline-secondary\" id=\"jobsPrev\" ${jobsPage<=0?'disabled':''}><i class=\"fa-solid fa-chevron-left\"></i> Prev</button><span class=\"small text-muted\">Page ${jobsPage+1} / ${pages} <i class=\"fa-solid fa-circle fa-2xs modal-meta-separator\" aria-hidden=\"true\"></i> ${jobsTotal} jobs</span><button class=\"btn btn-sm btn-outline-secondary\" id=\"jobsNext\" ${jobsPage>=pages-1?'disabled':''}>Next <i class=\"fa-solid fa-chevron-right\"></i></button></div>`; $('jobsPrev')?.addEventListener('click',()=>loadJobs(jobsPage-1)); $('jobsNext')?.addEventListener('click',()=>loadJobs(jobsPage+1)); }\n // Note: Job details now use the same opt-in visibility pattern as Logs, so large/bulk rows stay compact by default.\n $('jobsModal')?.addEventListener('show.bs.modal',()=>{ syncJobDetailsControl(); loadJobs(); }); $('refreshJobsBtn')?.addEventListener('click',()=>loadJobs()); $('jobsShowDetails')?.addEventListener('change',()=>{ saveJobDetailsPreference($('jobsShowDetails')?.checked === true); loadJobs(jobsPage); }); $('jobsTable')?.addEventListener('click',async e=>{ const btn=e.target.closest('.job-retry,.job-cancel,.job-force'); if(!btn)return; const id=btn.dataset.id; if(!id)return; if(btn.classList.contains('job-retry')) await post(`/api/jobs/${id}/retry`,{}).catch(x=>toast(x.message,'danger')); if(btn.classList.contains('job-force')){ if(!confirm('Force this pending job now in a separate worker? This can break normal queue ordering.')) return; await post(`/api/jobs/${id}/force`,{}).catch(x=>toast(x.message,'danger')); } if(btn.classList.contains('job-cancel')){ const st=btn.dataset.status||''; if((st==='pending'||st==='running') && !confirm('Emergency cancel this unfinished job?')) return; await post(`/api/jobs/${id}/cancel`,{}).catch(x=>toast(x.message,'danger')); } loadJobs(); });\n $('clearJobsBtn')?.addEventListener('click',async()=>{ if(!confirm('Clear finished job logs? Pending and running jobs will stay.')) return; try{ const j=await post('/api/jobs/clear',{}); toastMessage('toast.jobLogsCleared','success',{deleted:j.deleted}); jobsPage=0; loadJobs(0); }catch(e){ toast(e.message,'danger'); } });\n $('emergencyClearJobsBtn')?.addEventListener('click',async()=>{ if(!confirm('Emergency clean ALL job logs, including unfinished jobs?')) return; try{ const j=await post('/api/jobs/clear?force=1',{}); toastMessage('toast.emergencyJobLogsCleared','success',{deleted:j.deleted}); jobsPage=0; loadJobs(0); }catch(e){ toast(e.message,'danger'); } });\n\n";
|