`; $('jobsPrev')?.addEventListener('click',()=>loadJobs(jobsPage-1)); $('jobsNext')?.addEventListener('click',()=>loadJobs(jobsPage+1)); }
- // Note: Job log buttons depend on status: failed gets retry, while emergency cancel is only for pending/running.
+ // Note: Job log buttons are separated so normal cleanup cannot accidentally trigger emergency cleanup.
$('jobsModal')?.addEventListener('show.bs.modal',loadJobs); $('refreshJobsBtn')?.addEventListener('click',loadJobs); $('jobsTable')?.addEventListener('click',async e=>{ const btn=e.target.closest('.job-retry,.job-cancel'); 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-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(); });
- $('clearJobsBtn')?.addEventListener('click',async()=>{ const emergency=confirm('Emergency clear all job logs, including unfinished jobs? OK = emergency clear, Cancel = clear only finished logs.'); if(!emergency && !confirm('Clear finished job logs? Pending and running jobs will stay.')) return; try{ const j=await post(`/api/jobs/clear${emergency?'?force=1':''}`,{}); toast(`${emergency?'Emergency cleared':'Cleared'} ${j.deleted||0} job log(s)`,'success'); jobsPage=0; loadJobs(0); }catch(e){ toast(e.message,'danger'); } });
+ $('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',{}); toast(`Cleared ${j.deleted||0} finished job log(s)`,'success'); jobsPage=0; loadJobs(0); }catch(e){ toast(e.message,'danger'); } });
+ $('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',{}); toast(`Emergency cleared ${j.deleted||0} job log(s)`,'success'); jobsPage=0; loadJobs(0); }catch(e){ toast(e.message,'danger'); } });
async function loadLabels(){ const j=await (await fetch('/api/labels')).json(); const labels=j.labels||[]; knownLabels=labels; renderLabelFilters(); renderLabelChooser(); if($('labelsManager')) $('labelsManager').innerHTML=labels.length?labels.map(l=>`
${esc(l.name)}
`).join(''):'No labels.'; }
function renderLabelChooser(){ if($('selectedLabelList')) $('selectedLabelList').innerHTML=[...modalLabels].map(l=>``).join('') || 'No labels selected.'; if($('labelList')) $('labelList').innerHTML=knownLabels.map(l=>``).join('') || 'No saved labels.'; }
@@ -534,6 +539,8 @@
const cond={type, negate:!!$('autoCondNegate')?.checked};
if(type==='no_seeds'){ cond.seeds=Number($('autoCondSeeds')?.value||0); cond.minutes=Number($('autoCondMinutes')?.value||0); }
if(type==='ratio_gte') cond.ratio=Number($('autoCondRatio')?.value||1);
+ // Note: Progress conditions compare the torrent completion percentage stored in the live torrent row.
+ if(type==='progress_gte'||type==='progress_lte') cond.progress=Number($('autoCondProgress')?.value||0);
if(type==='label_missing'||type==='label_has') cond.label=$('autoCondLabel')?.value||'';
if(type==='status') cond.status=$('autoCondStatus')?.value||'Seeding';
if(type==='path_contains') cond.text=$('autoCondText')?.value||'';
@@ -562,7 +569,7 @@
}
function conditionText(c={}){
- const base=c.type==='no_seeds'?`seeds <= ${c.seeds||0} for ${c.minutes||0} min`:c.type==='ratio_gte'?`ratio >= ${c.ratio}`:c.type==='label_missing'?`missing label ${c.label||''}`:c.type==='label_has'?`has label ${c.label||''}`:c.type==='status'?`status = ${c.status||''}`:c.type==='path_contains'?`path contains ${c.text||''}`:'completed';
+ const base=c.type==='no_seeds'?`seeds <= ${c.seeds||0} for ${c.minutes||0} min`:c.type==='ratio_gte'?`ratio >= ${c.ratio}`:c.type==='progress_gte'?`progress >= ${c.progress||0}%`:c.type==='progress_lte'?`progress <= ${c.progress||0}%`:c.type==='label_missing'?`missing label ${c.label||''}`:c.type==='label_has'?`has label ${c.label||''}`:c.type==='status'?`status = ${c.status||''}`:c.type==='path_contains'?`path contains ${c.text||''}`:'completed';
return c.negate?`NOT (${base})`:base;
}
function effectText(e={}){
@@ -637,9 +644,9 @@
function renderAutomationHistory(hist=[]){
if(!$('automationHistory')) return;
const toolbar='';
- const rows=hist.map(h=>[esc(h.created_at),esc(h.rule_name||''),esc(h.torrent_name||h.torrent_hash||''),automationHistoryActions(h.actions_json||'')]);
- // Note: Automation history uses its own table class so long action JSON wraps inside the modal.
- const body=hist.length?table(['Time','Rule','Torrent / batch','Actions'],rows).replace('detail-table','detail-table automation-history-table'):'
No automation history yet.
';
+ const rows=hist.map(h=>[humanDateCell(h.created_at),esc(h.rule_name||''),esc(h.torrent_name||h.torrent_hash||''),automationHistoryActions(h.actions_json||'')]);
+ // Note: Automation history time is now human-readable and wider, while the table still wraps on small screens.
+ const body=hist.length?`
0 means unlimited. Sliders use Mbit/s and save through the existing speed limits API.
-
Job queue
Pending, running, done, failed, retry and cancel history.
Loading jobs...
+
Job queue
Pending, running, done, failed, retry and cancel history.
Loading jobs...
Choose path
Loading...
Unchecked: only rTorrent path is changed. Checked: torrent is stopped/closed, files are moved one by one by the job queue, path is updated, then torrent is started again if it was active.
@@ -148,7 +148,7 @@
Set ratio group
-
Tools & rTorrents
{% if auth_enabled and current_user and current_user.role == 'admin' %}{% endif %}
rTorrents
Loading profiles...
Add rTorrent profile
Create one rTorrent profile at a time. Move/remove queues keep their order for each profile.
Torrent statistics
Cached metadata summary. File metadata is refreshed every 15 minutes, a few minutes after startup, or manually.
Not loaded.
Open this tab to load statistics.
Appearance
Theme and typography.
Port checker
Incoming connection test, separate from visual preferences.
disabled
Uses YouGetSignal first. Manual check bypasses the 6h cache.
Footer
Choose which status items are visible in the bottom bar.
{% if auth_enabled and current_user and current_user.role == 'admin' %}
Users
Manage optional pyTorrent users. Empty profile means all profiles. R/O blocks rTorrent-changing actions; Full allows them.
{% endif %}
rTorrent config
Typical rTorrent options, like in ruTorrent. Unsupported methods are shown as unavailable.
Reference value is kept from the first override save. Later saves add or clear differences without replacing the original reference.
No changes
Loading config...
Cleanup / retention
One place to review and clear job logs and Smart Queue history. Pending and running jobs are preserved.
Loading cleanup data...
pyTorrent status
Diagnostics for pyTorrent process and active SCGI/XMLRPC connection.
Open this tab to load diagnostics.
Choose columns visible in the torrent list.
Smart Queue
Automatic queue balancing for slow or stalled downloads.
Run Smart Queue automatically during polling.
Off = only paused torrents are managed.
Torrents excluded below are ignored by Smart Queue and continue normally.
Last operations
Automations / rules
Build a rule as: conditions first, then ordered actions. Matching torrents are handled as one batch and the cooldown is applied to the whole rule.
1. Rule
2. Conditions
3. Actions, in order
Rules
History
+
Tools & rTorrents
{% if auth_enabled and current_user and current_user.role == 'admin' %}{% endif %}
rTorrents
Loading profiles...
Add rTorrent profile
Create one rTorrent profile at a time. Move/remove queues keep their order for each profile.
Torrent statistics
Cached metadata summary. File metadata is refreshed every 15 minutes, a few minutes after startup, or manually.
Not loaded.
Open this tab to load statistics.
Appearance
Theme and typography.
Port checker
Incoming connection test, separate from visual preferences.
disabled
Uses YouGetSignal first. Manual check bypasses the 6h cache.
Footer
Choose which status items are visible in the bottom bar.
{% if auth_enabled and current_user and current_user.role == 'admin' %}
Users
Manage optional pyTorrent users. Empty profile means all profiles. R/O blocks rTorrent-changing actions; Full allows them.
{% endif %}
rTorrent config
Typical rTorrent options, like in ruTorrent. Unsupported methods are shown as unavailable.
Reference value is kept from the first override save. Later saves add or clear differences without replacing the original reference.
No changes
Loading config...
Cleanup / retention
One place to review and clear job logs and Smart Queue history. Pending and running jobs are preserved.
Loading cleanup data...
pyTorrent status
Diagnostics for pyTorrent process and active SCGI/XMLRPC connection.
Open this tab to load diagnostics.
Choose columns visible in the torrent list.
Smart Queue
Automatic queue balancing for slow or stalled downloads.
Run Smart Queue automatically during polling.
Off = only paused torrents are managed.
Torrents excluded below are ignored by Smart Queue and continue normally.
Last operations
Automations / rules
Build a rule as: conditions first, then ordered actions. Matching torrents are handled as one batch and the cooldown is applied to the whole rule.