ux
This commit is contained in:
@@ -862,14 +862,13 @@
|
|||||||
return `<details class="automation-history-details"><summary>${summary||'No actions'}</summary><pre>${details}</pre></details>`;
|
return `<details class="automation-history-details"><summary>${summary||'No actions'}</summary><pre>${details}</pre></details>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAutomationHistory(hist=[], smartStats=automationSmartQueueStats){
|
function renderAutomationHistory(hist=[]){
|
||||||
if(!$('automationHistory')) return;
|
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 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||'')]);
|
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.
|
// 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>';
|
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=stats+toolbar+body;
|
$('automationHistory').innerHTML=toolbar+body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearAutomationHistory(){
|
async function clearAutomationHistory(){
|
||||||
@@ -893,13 +892,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadAutomations(){
|
async function loadAutomations(){
|
||||||
const [j,smart]=await Promise.all([
|
const j=await fetch('/api/automations').then(r=>r.json());
|
||||||
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||[];
|
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;
|
automationRulesCache=rules;
|
||||||
if($('automationManager')) $('automationManager').innerHTML=rules.length?rules.map(r=>{
|
if($('automationManager')) $('automationManager').innerHTML=rules.length?rules.map(r=>{
|
||||||
const enabled=!!r.enabled;
|
const enabled=!!r.enabled;
|
||||||
@@ -908,7 +902,7 @@
|
|||||||
const toggleClass=enabled?'btn-outline-warning':'btn-outline-success';
|
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>`;
|
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>';
|
}).join(''):'<div class="empty-mini">No automation rules.</div>';
|
||||||
renderAutomationHistory(hist, automationSmartQueueStats);
|
renderAutomationHistory(hist);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAutomationRule(rule){
|
async function toggleAutomationRule(rule){
|
||||||
@@ -1082,10 +1076,14 @@
|
|||||||
const box=$('appStatusManager'); if(!box) return;
|
const box=$('appStatusManager'); if(!box) return;
|
||||||
box.innerHTML='<span class="spinner-border spinner-border-sm"></span> Loading diagnostics...';
|
box.innerHTML='<span class="spinner-border spinner-border-sm"></span> Loading diagnostics...';
|
||||||
try{
|
try{
|
||||||
const j=await (await fetch('/api/app/status')).json();
|
const [j,smart]=await Promise.all([
|
||||||
|
fetch('/api/app/status').then(r=>r.json()),
|
||||||
|
fetch('/api/smart-queue?history_limit=100').then(r=>r.json()).catch(()=>({ok:false}))
|
||||||
|
]);
|
||||||
if(!j.ok) throw new Error(j.error||'Failed to load diagnostics');
|
if(!j.ok) throw new Error(j.error||'Failed to load diagnostics');
|
||||||
const st=j.status||{}, py=st.pytorrent||{}, scgi=st.scgi||{}, profile=st.profile||{}, pc=st.port_check||{}, cleanup=st.cleanup||{}, db=cleanup.database||{};
|
const st=j.status||{}, py=st.pytorrent||{}, scgi=st.scgi||{}, profile=st.profile||{}, pc=st.port_check||{}, cleanup=st.cleanup||{}, db=cleanup.database||{};
|
||||||
const peaks=st.speed_peaks||{}, peakSession=peaks.session||{}, peakAllTime=peaks.all_time||{};
|
const peaks=st.speed_peaks||{}, peakSession=peaks.session||{}, peakAllTime=peaks.all_time||{};
|
||||||
|
const smartStats=smart?.ok?buildSmartQueueNerdStats(smart.history||[], Number(smart.history_total||0)):null;
|
||||||
const cards=[
|
const cards=[
|
||||||
diagCard('pyTorrent PID', py.pid), diagCard('pyTorrent uptime', `${py.uptime_seconds||0}s`), diagCard('Memory RSS', py.memory_rss_h||py.memory_rss),
|
diagCard('pyTorrent PID', py.pid), diagCard('pyTorrent uptime', `${py.uptime_seconds||0}s`), diagCard('Memory RSS', py.memory_rss_h||py.memory_rss),
|
||||||
diagCard('Threads', py.threads), diagCard('CPU', `${py.cpu_percent ?? '-'}%`), diagCard('Jobs total', py.jobs_total),
|
diagCard('Threads', py.threads), diagCard('CPU', `${py.cpu_percent ?? '-'}%`), diagCard('Jobs total', py.jobs_total),
|
||||||
@@ -1098,8 +1096,9 @@
|
|||||||
diagCard('SCGI first byte', scgi.first_byte_ms!=null?`${scgi.first_byte_ms} ms`:'-'), diagCard('SCGI total', scgi.total_ms!=null?`${scgi.total_ms} ms`:'-'),
|
diagCard('SCGI first byte', scgi.first_byte_ms!=null?`${scgi.first_byte_ms} ms`:'-'), diagCard('SCGI total', scgi.total_ms!=null?`${scgi.total_ms} ms`:'-'),
|
||||||
diagCard('Request bytes', scgi.request_bytes), diagCard('Response bytes', scgi.response_bytes), diagCard('XML bytes', scgi.xml_bytes), diagCard('rTorrent version', scgi.client_version||'-')
|
diagCard('Request bytes', scgi.request_bytes), diagCard('Response bytes', scgi.response_bytes), diagCard('XML bytes', scgi.xml_bytes), diagCard('rTorrent version', scgi.client_version||'-')
|
||||||
];
|
];
|
||||||
box.innerHTML=`<div class="diag-grid">${cards.join('')}</div>${scgi.error?`<div class="alert alert-danger mt-3 mb-0">${esc(scgi.error)}</div>`:''}`;
|
const smartBlock=`<div class="section-title mt-3"><i class="fa-solid fa-list-check"></i> Smart Queue statistics</div>${renderSmartQueueNerdStats(smartStats)}`;
|
||||||
}catch(e){ box.innerHTML=`<div class="text-danger">${esc(e.message)}</div>`; }
|
box.innerHTML=`<div class="diag-grid">${cards.join('')}</div>${smartBlock}${scgi.error?`<div class="alert alert-danger mt-3 mb-0">${esc(scgi.error)}</div>`:''}`;
|
||||||
|
}catch(e){ box.innerHTML=`<div class="alert alert-danger mb-0">${esc(e.message)}</div>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentStatsCard(label, value, note=''){
|
function torrentStatsCard(label, value, note=''){
|
||||||
|
|||||||
@@ -1425,7 +1425,7 @@ body.mobile-mode .mobile-card {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
/* Note: Smart Queue nerd stats are scoped to Automations to avoid reusing or overriding generic cards. */
|
/* Note: Smart Queue stats are reusable because they are shown in App status. */
|
||||||
.automation-smart-stats {
|
.automation-smart-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
@@ -2326,6 +2326,34 @@ body.mobile-mode .mobile-filter-bar {
|
|||||||
color: var(--bs-secondary-color);
|
color: var(--bs-secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.about-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-summary-grid div {
|
||||||
|
padding: 0.7rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: rgba(var(--bs-secondary-bg-rgb), 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-summary-grid b,
|
||||||
|
.about-summary-grid span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-summary-grid b {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-summary-grid span {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
.about-list {
|
.about-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="modal fade" id="aboutModal" tabindex="-1" aria-labelledby="aboutModalLabel" aria-hidden="true">
|
<div class="modal fade" id="aboutModal" tabindex="-1" aria-labelledby="aboutModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<div class="modal-content about-modal-content">
|
<div class="modal-content about-modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 id="aboutModalLabel" class="modal-title"><i class="fa-solid fa-robot"></i> About pyTorrent</h5>
|
<h5 id="aboutModalLabel" class="modal-title"><i class="fa-solid fa-robot"></i> About pyTorrent</h5>
|
||||||
@@ -168,15 +168,22 @@
|
|||||||
<div class="about-logo"><i class="fa-solid fa-robot"></i></div>
|
<div class="about-logo"><i class="fa-solid fa-robot"></i></div>
|
||||||
<div>
|
<div>
|
||||||
<h6>pyTorrent</h6>
|
<h6>pyTorrent</h6>
|
||||||
<p>Lightweight web panel for rTorrent management.</p>
|
<p>Lightweight web panel for rTorrent management, queue control and live torrent diagnostics.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="about-summary-grid mb-3">
|
||||||
|
<div><b>rTorrent panel</b><span>SCGI connection, live table, labels, ratios and tracker tools.</span></div>
|
||||||
|
<div><b>Automation</b><span>Rules, Smart Queue, RSS checks, cleanup and job queue support.</span></div>
|
||||||
|
<div><b>Operations</b><span>App status, port checks, traffic history and configurable UI preferences.</span></div>
|
||||||
|
</div>
|
||||||
<dl class="about-list">
|
<dl class="about-list">
|
||||||
|
<div><dt>Repository</dt><dd><a href="https://git.linuxiarz.pl/gru/pyTorrent" target="_blank" rel="noopener noreferrer"><i class="fa-brands fa-git-alt"></i> git.linuxiarz.pl/gru/pyTorrent</a></dd></div>
|
||||||
<div><dt>License</dt><dd>Open source</dd></div>
|
<div><dt>License</dt><dd>Open source</dd></div>
|
||||||
<div><dt>Author</dt><dd>linuxiarz.pl</dd></div>
|
<div><dt>Author</dt><dd>linuxiarz.pl</dd></div>
|
||||||
<div><dt>Backend</dt><dd>Python, Flask, Flask-SocketIO</dd></div>
|
<div><dt>Backend</dt><dd>Python, Flask, Flask-SocketIO, SQLite</dd></div>
|
||||||
<div><dt>Frontend</dt><dd>Bootstrap, vanilla JavaScript, Font Awesome</dd></div>
|
<div><dt>Frontend</dt><dd>Bootstrap, vanilla JavaScript, Chart.js, Font Awesome</dd></div>
|
||||||
<div><dt>Runtime</dt><dd>Gunicorn compatible, rTorrent over SCGI</dd></div>
|
<div><dt>Runtime</dt><dd>Gunicorn compatible, systemd-ready, rTorrent over SCGI</dd></div>
|
||||||
|
<div><dt>Features</dt><dd>Magnet and torrent upload, file priorities, labels, ratio groups, Smart Queue, automation rules, RSS, traffic charts and diagnostics.</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user