This commit is contained in:
Mateusz Gruszczyński
2026-05-08 18:13:32 +02:00
parent cd6c4fad85
commit 76412284bb
3 changed files with 99 additions and 10 deletions

View File

@@ -12,6 +12,7 @@
let hiddenColumns = new Set((window.PYTORRENT?.tableColumns?.hidden || []));
let knownLabels = [];
let jobsPage = 0, jobsLimit = 25, jobsTotal = 0, smartHistoryExpanded = false;
let automationSmartQueueStats = null;
let peersRefreshTimer = null;
let peersRefreshSeconds = Number(window.PYTORRENT?.peersRefreshSeconds || 0);
let portCheckEnabled = !!Number(window.PYTORRENT?.portCheckEnabled || 0);
@@ -421,6 +422,41 @@
function smartHistoryDetails(row){ try{ return typeof row.details_json==='string'?JSON.parse(row.details_json||'{}'):(row.details_json||{}); }catch(e){ return {}; } }
function smartQueueToastMessage(r){ const noEffect=r.start_no_effect?.length||0; const requested=r.start_requested?.length||0; const stopFailed=r.stop_failed?.length||0; const limit=r.max_active_downloads||r.settings?.max_active_downloads||''; const activeBefore=r.active_before; const activeAfter=r.active_after_stop ?? r.active_after_expected; const activeTail=activeBefore!==undefined?`, active ${esc(activeBefore)}->${esc(activeAfter ?? '?')}${limit?`/${esc(limit)}`:''}`:''; const cap=r.rtorrent_cap?.updated?`, cap ${r.rtorrent_cap.current}->${r.rtorrent_cap.new}`:''; const waiting=r.waiting_labeled||0; const stalled=r.stalled_labeled?.length||0; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; const waitTail=waiting?`, waiting labeled ${waiting}`:''; const stalledTail=stalled?`, stalled ${stalled}`:''; const failTail=stopFailed?`, stop failed ${stopFailed}`:''; return `Smart Queue: stopped ${r.stopped?.length||r.paused?.length||0}, started ${r.started?.length||r.resumed?.length||0}${activeTail}${tail}${waitTail}${stalledTail}${failTail}${cap}`; }
function buildSmartQueueNerdStats(hist=[], totalHistory=0){
// Note: Small Smart Queue telemetry for automation nerds; it reads history only and does not affect queue behavior.
const stats=hist.reduce((acc,h)=>{
const details=smartHistoryDetails(h);
const stopped=Number(h.paused_count||0);
const started=Number(h.resumed_count||0);
const checked=Number(h.checked_count||0);
const over=Number(details.over_limit||0);
const stopFailed=Array.isArray(details.stop_failed)?details.stop_failed.length:0;
acc.checked += checked;
acc.stopped += stopped;
acc.started += started;
acc.overLimit += over;
acc.stopFailed += stopFailed;
if(over>0) acc.overEvents += 1;
return acc;
},{checked:0,stopped:0,started:0,overLimit:0,overEvents:0,stopFailed:0});
const latest=hist[0]||null;
return {...stats,total:Number(totalHistory||hist.length||0),sample:hist.length,latestEvent:latest?.event||'-',latestAt:latest?.created_at||''};
}
function renderSmartQueueNerdStats(stats){
// Note: Compact cards keep the extra diagnostics readable above Automation history without changing the history table.
if(!stats) return '<div class="automation-smart-stats empty-mini">No Smart Queue stats yet.</div>';
const cards=[
['Runs',stats.total,`${stats.sample} loaded`],
['Checked',stats.checked,'torrent scans'],
['Stopped',stats.stopped,'queue trims'],
['Started',stats.started,'queue fills'],
['Over limit',stats.overEvents,`${stats.overLimit} total over`],
['Stop failed',stats.stopFailed,'rTorrent rejects'],
['Latest',stats.latestEvent,stats.latestAt?dateCell(stats.latestAt):'no timestamp'],
];
return `<div class="automation-smart-stats" aria-label="Smart Queue nerd stats">${cards.map(([label,value,hint])=>`<div class="automation-smart-stat"><span>${esc(label)}</span><b>${esc(value)}</b><small>${hint}</small></div>`).join('')}</div>`;
}
async function loadSmartQueue(){
if($('smartManager')) $('smartManager').innerHTML=loadingMarkup('Loading Smart Queue...');
if($('smartHistory')) $('smartHistory').innerHTML=loadingMarkup('Loading Smart Queue history...');
@@ -702,13 +738,14 @@
return `<details class="automation-history-details"><summary>${summary||'No actions'}</summary><pre>${details}</pre></details>`;
}
function renderAutomationHistory(hist=[]){
function renderAutomationHistory(hist=[], smartStats=automationSmartQueueStats){
if(!$('automationHistory')) return;
const stats=renderSmartQueueNerdStats(smartStats);
const toolbar='<div class="automation-history-toolbar"><button id="automationClearHistoryBtn" class="btn btn-xs btn-outline-danger" type="button"><i class="fa-solid fa-trash"></i> Clear history</button></div>';
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 uses the shared responsive table wrapper so it stays inside narrow mobile modals.
const body=hist.length?responsiveTable(['Time','Rule','Torrent / batch','Actions'],rows,'automation-history-table'):'<div class="empty-mini">No automation history yet.</div>';
$('automationHistory').innerHTML=toolbar+body;
$('automationHistory').innerHTML=stats+toolbar+body;
}
async function clearAutomationHistory(){
@@ -732,8 +769,13 @@
}
async function loadAutomations(){
const j=await (await fetch('/api/automations')).json();
const [j,smart]=await Promise.all([
fetch('/api/automations').then(r=>r.json()),
fetch('/api/smart-queue?history_limit=100').then(r=>r.json()).catch(()=>({}))
]);
const rules=j.rules||[], hist=j.history||[];
// Note: Automations only display Smart Queue diagnostics here; saving/checking rules remains unchanged.
automationSmartQueueStats=smart?.ok?buildSmartQueueNerdStats(smart.history||[], Number(smart.history_total||0)):null;
automationRulesCache=rules;
if($('automationManager')) $('automationManager').innerHTML=rules.length?rules.map(r=>{
const enabled=!!r.enabled;
@@ -742,7 +784,7 @@
const toggleClass=enabled?'btn-outline-warning':'btn-outline-success';
return `<div class="automation-row"><div class="automation-row-main"><div><b>${esc(r.name)}</b> ${enabled?'<span class="badge text-bg-success">on</span>':'<span class="badge text-bg-secondary">off</span>'}</div><div class="small text-muted automation-rule-summary">${esc(ruleSummary(r))} · cooldown ${esc(r.cooldown_minutes||0)} min</div></div><div class="automation-row-actions"><button class="btn btn-xs ${toggleClass} automation-toggle" data-id="${esc(r.id)}" type="button" title="${toggleTitle}"><i class="fa-solid ${toggleIcon}"></i></button><button class="btn btn-xs btn-outline-secondary automation-edit" data-id="${esc(r.id)}" type="button" title="Edit automation"><i class="fa-solid fa-pen"></i></button><button class="btn btn-xs btn-outline-danger automation-delete" data-id="${esc(r.id)}" type="button" title="Delete automation"><i class="fa-solid fa-trash"></i></button></div></div>`;
}).join(''):'<div class="empty-mini">No automation rules.</div>';
renderAutomationHistory(hist);
renderAutomationHistory(hist, automationSmartQueueStats);
}
async function toggleAutomationRule(rule){