From 76412284bbbf165eb4442f4bc04badce23c3167a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 8 May 2026 18:13:32 +0200 Subject: [PATCH] ux fix --- pytorrent/static/app.js | 50 ++++++++++++++++++++++++++--- pytorrent/static/styles.css | 57 +++++++++++++++++++++++++++++++--- pytorrent/templates/index.html | 2 +- 3 files changed, 99 insertions(+), 10 deletions(-) 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 @@ - +