diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js
index 0da60e9..bc36d47 100644
--- a/pytorrent/static/app.js
+++ b/pytorrent/static/app.js
@@ -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 '
No Smart Queue stats yet.
';
+ 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 `${cards.map(([label,value,hint])=>`
${esc(label)}${esc(value)}${hint}
`).join('')}
`;
+ }
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 `${summary||'No actions'}
${details} `;
}
- function renderAutomationHistory(hist=[]){
+ function renderAutomationHistory(hist=[], smartStats=automationSmartQueueStats){
if(!$('automationHistory')) return;
+ const stats=renderSmartQueueNerdStats(smartStats);
const toolbar='';
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'):'No automation history yet.
';
- $('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 `${esc(r.name)} ${enabled?'on':'off'}
${esc(ruleSummary(r))} ยท cooldown ${esc(r.cooldown_minutes||0)} min
`;
}).join(''):'No automation rules.
';
- renderAutomationHistory(hist);
+ renderAutomationHistory(hist, automationSmartQueueStats);
}
async function toggleAutomationRule(rule){
diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css
index 209b75a..30f0c86 100644
--- a/pytorrent/static/styles.css
+++ b/pytorrent/static/styles.css
@@ -677,18 +677,32 @@ body {
font-weight: 700;
text-transform: uppercase;
}
+/* Note: Browser title speed preference uses a two-column switch layout, so text aligns with the switch. */
.browser-speed-pref {
display: grid;
- gap: 0.25rem;
- align-content: center;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ column-gap: 0.75rem;
+ row-gap: 0.2rem;
min-height: 58px;
margin: 0;
- padding: 0.55rem 0.75rem 0.55rem 2.6rem;
+ padding: 0.55rem 0.75rem;
border: 1px solid var(--bs-border-color);
border-radius: 0.65rem;
background: rgba(var(--bs-secondary-bg-rgb), 0.35);
}
+.browser-speed-pref .form-check-input {
+ grid-row: 1 / span 2;
+ grid-column: 1;
+ float: none;
+ margin: 0;
+}
+.browser-speed-pref .form-check-label {
+ grid-column: 2;
+ line-height: 1.2;
+}
.browser-speed-pref small {
+ grid-column: 2;
color: var(--bs-secondary-color);
line-height: 1.2;
}
@@ -1359,6 +1373,35 @@ body.mobile-mode .mobile-card {
white-space: normal;
word-break: break-word;
}
+/* Note: Smart Queue nerd stats are scoped to Automations to avoid reusing or overriding generic cards. */
+.automation-smart-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: 0.5rem;
+ margin: 0.5rem 0 0.75rem;
+}
+.automation-smart-stat {
+ min-width: 0;
+ padding: 0.5rem 0.6rem;
+ border: 1px solid var(--bs-border-color);
+ border-radius: 0.6rem;
+ background: rgba(var(--bs-secondary-bg-rgb), 0.28);
+}
+.automation-smart-stat span,
+.automation-smart-stat small {
+ display: block;
+ color: var(--bs-secondary-color);
+ font-size: 0.72rem;
+ line-height: 1.2;
+}
+.automation-smart-stat b {
+ display: block;
+ overflow: hidden;
+ font-size: 1rem;
+ line-height: 1.3;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
.automation-history-toolbar {
display: flex;
justify-content: flex-end;
@@ -2087,16 +2130,20 @@ body.mobile-mode .mobile-filter-bar {
background: rgba(var(--bs-secondary-bg-rgb), 0.28);
}
+/* Note: Smart Queue switch resets Bootstrap's negative switch offset so it cannot overflow narrow frames. */
.smart-toggle-row .form-check {
display: flex;
align-items: center;
+ justify-content: flex-end;
+ flex: 0 0 auto;
min-height: 0;
margin: 0;
- padding-left: 2.25rem;
+ padding-left: 0;
}
.smart-toggle-row .form-check-input {
margin-top: 0;
+ margin-left: 0;
}
.smart-setting-row .form-check-label,
@@ -2171,7 +2218,7 @@ body.mobile-mode .mobile-filter-bar {
}
.smart-toggle-row .form-check {
- padding-left: 0;
+ justify-content: flex-start;
}
}
diff --git a/pytorrent/templates/index.html b/pytorrent/templates/index.html
index 25a9be4..327229f 100644
--- a/pytorrent/templates/index.html
+++ b/pytorrent/templates/index.html
@@ -148,7 +148,7 @@
-
+