checkport fix
This commit is contained in:
@@ -62,16 +62,53 @@ def _public_ip(profile: dict | None = None, force: bool = False) -> str:
|
|||||||
return res.read(64).decode("utf-8", "replace").strip()
|
return res.read(64).decode("utf-8", "replace").strip()
|
||||||
|
|
||||||
|
|
||||||
def _incoming_port(profile: dict) -> int | None:
|
MAX_PORT_CHECK_CANDIDATES = 256
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_port_candidates(value: str, limit: int = MAX_PORT_CHECK_CANDIDATES) -> tuple[list[int], bool]:
|
||||||
|
"""Return valid incoming port candidates from rTorrent network.port_range.
|
||||||
|
|
||||||
|
Note: rTorrent may keep a range/list and pick a random port on start.
|
||||||
|
The old checker used only the first number, which produced false "closed"
|
||||||
|
results when another configured port was actually active.
|
||||||
|
"""
|
||||||
|
ports: list[int] = []
|
||||||
|
seen: set[int] = set()
|
||||||
|
truncated = False
|
||||||
|
|
||||||
|
def add(port: int) -> None:
|
||||||
|
nonlocal truncated
|
||||||
|
if not 1 <= port <= 65535 or port in seen:
|
||||||
|
return
|
||||||
|
if len(ports) >= limit:
|
||||||
|
truncated = True
|
||||||
|
return
|
||||||
|
seen.add(port)
|
||||||
|
ports.append(port)
|
||||||
|
|
||||||
|
for start, end in re.findall(r"(\d{1,5})\s*-\s*(\d{1,5})", value or ""):
|
||||||
|
a, b = int(start), int(end)
|
||||||
|
if a > b:
|
||||||
|
a, b = b, a
|
||||||
|
for port in range(a, b + 1):
|
||||||
|
add(port)
|
||||||
|
if truncated:
|
||||||
|
break
|
||||||
|
|
||||||
|
without_ranges = re.sub(r"\d{1,5}\s*-\s*\d{1,5}", " ", value or "")
|
||||||
|
for item in re.findall(r"\d{1,5}", without_ranges):
|
||||||
|
add(int(item))
|
||||||
|
|
||||||
|
return ports, truncated
|
||||||
|
|
||||||
|
|
||||||
|
def _incoming_ports(profile: dict) -> dict:
|
||||||
try:
|
try:
|
||||||
value = str(rtorrent.client_for(profile).call("network.port_range") or "")
|
raw_value = str(rtorrent.client_for(profile).call("network.port_range") or "")
|
||||||
except Exception:
|
except Exception:
|
||||||
value = ""
|
raw_value = ""
|
||||||
match = re.search(r"(\d{2,5})", value)
|
ports, truncated = _parse_port_candidates(raw_value)
|
||||||
if not match:
|
return {"ports": ports, "raw": raw_value, "truncated": truncated}
|
||||||
return None
|
|
||||||
port = int(match.group(1))
|
|
||||||
return port if 1 <= port <= 65535 else None
|
|
||||||
|
|
||||||
|
|
||||||
def _yougetsignal_check(public_ip: str, port: int) -> dict:
|
def _yougetsignal_check(public_ip: str, port: int) -> dict:
|
||||||
@@ -104,16 +141,40 @@ def _local_port_fallback(public_ip: str, port: int) -> dict:
|
|||||||
return {"status": "unknown", "source": "local-fallback", "error": f"Local fallback inconclusive: {exc}"}
|
return {"status": "unknown", "source": "local-fallback", "error": f"Local fallback inconclusive: {exc}"}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_ports(public_ip: str, ports: list[int], checker) -> dict:
|
||||||
|
checked: list[int] = []
|
||||||
|
first_closed: dict | None = None
|
||||||
|
last_result: dict = {"status": "unknown"}
|
||||||
|
|
||||||
|
for port in ports:
|
||||||
|
checked.append(port)
|
||||||
|
current = checker(public_ip, port)
|
||||||
|
last_result = current
|
||||||
|
if current.get("status") == "open":
|
||||||
|
current.update({"port": port, "open_port": port, "checked_ports": checked})
|
||||||
|
return current
|
||||||
|
if current.get("status") == "closed" and first_closed is None:
|
||||||
|
first_closed = current
|
||||||
|
|
||||||
|
result = first_closed or last_result
|
||||||
|
result.update({"port": ports[0] if ports else None, "open_port": None, "checked_ports": checked})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def port_check_status(force: bool = False) -> dict:
|
def port_check_status(force: bool = False) -> dict:
|
||||||
profile = preferences.active_profile()
|
profile = preferences.active_profile()
|
||||||
prefs = preferences.get_preferences()
|
prefs = preferences.get_preferences()
|
||||||
enabled = bool((prefs or {}).get("port_check_enabled"))
|
enabled = bool((prefs or {}).get("port_check_enabled"))
|
||||||
if not profile:
|
if not profile:
|
||||||
return {"status": "unknown", "enabled": enabled, "error": "No profile"}
|
return {"status": "unknown", "enabled": enabled, "error": "No profile"}
|
||||||
port = _incoming_port(profile)
|
|
||||||
if not port:
|
port_info = _incoming_ports(profile)
|
||||||
|
ports = port_info["ports"]
|
||||||
|
if not ports:
|
||||||
return {"status": "unknown", "enabled": enabled, "error": "Cannot read rTorrent network.port_range"}
|
return {"status": "unknown", "enabled": enabled, "error": "Cannot read rTorrent network.port_range"}
|
||||||
cache_key = f"port_check:{profile['id']}:{port}"
|
|
||||||
|
ports_key = ",".join(str(port) for port in ports)
|
||||||
|
cache_key = f"port_check:{profile['id']}:{ports_key}:{int(bool(port_info['truncated']))}"
|
||||||
if not force:
|
if not force:
|
||||||
cached = _app_setting_get(cache_key)
|
cached = _app_setting_get(cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
@@ -127,20 +188,31 @@ def port_check_status(force: bool = False) -> dict:
|
|||||||
return data
|
return data
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
checked_at_epoch = time.time()
|
checked_at_epoch = time.time()
|
||||||
result = {"status": "unknown", "enabled": enabled, "port": port, "checked_at_epoch": checked_at_epoch, "checked_at": _iso_from_epoch(checked_at_epoch), "cached": False}
|
result = {
|
||||||
|
"status": "unknown",
|
||||||
|
"enabled": enabled,
|
||||||
|
"port": ports[0],
|
||||||
|
"ports": ports,
|
||||||
|
"port_range": port_info["raw"],
|
||||||
|
"ports_truncated": port_info["truncated"],
|
||||||
|
"checked_at_epoch": checked_at_epoch,
|
||||||
|
"checked_at": _iso_from_epoch(checked_at_epoch),
|
||||||
|
"cached": False,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
public_ip = _public_ip(profile, force=force)
|
public_ip = _public_ip(profile, force=force)
|
||||||
result["public_ip"] = public_ip
|
result["public_ip"] = public_ip
|
||||||
result["remote"] = bool(profile.get("is_remote"))
|
result["remote"] = bool(profile.get("is_remote"))
|
||||||
result.update(_yougetsignal_check(public_ip, port))
|
result.update(_check_ports(public_ip, ports, _yougetsignal_check))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
result["error"] = f"YouGetSignal failed: {exc}"
|
result["error"] = f"YouGetSignal failed: {exc}"
|
||||||
try:
|
try:
|
||||||
public_ip = result.get("public_ip") or _public_ip(profile, force=force)
|
public_ip = result.get("public_ip") or _public_ip(profile, force=force)
|
||||||
result["public_ip"] = public_ip
|
result["public_ip"] = public_ip
|
||||||
result["remote"] = bool(profile.get("is_remote"))
|
result["remote"] = bool(profile.get("is_remote"))
|
||||||
result.update(_local_port_fallback(public_ip, port))
|
result.update(_check_ports(public_ip, ports, _local_port_fallback))
|
||||||
except Exception as fallback_exc:
|
except Exception as fallback_exc:
|
||||||
result["fallback_error"] = str(fallback_exc)
|
result["fallback_error"] = str(fallback_exc)
|
||||||
result["source"] = "none"
|
result["source"] = "none"
|
||||||
|
|||||||
@@ -299,7 +299,7 @@
|
|||||||
async function browsePath(path){ $('pathList').innerHTML='<span class="spinner-border spinner-border-sm"></span> Loading...'; try{ const res=await fetch(`/api/path/browse?path=${encodeURIComponent(path||'/')}`); const j=await res.json(); if(!j.ok) throw new Error(j.error); $('pathCurrent').value=j.path; lastPathParent=j.parent; $('pathList').innerHTML=j.dirs.map(d=>`<div class="path-row" data-path="${esc(d.path)}"><i class="fa-solid fa-folder"></i><span>${esc(d.name)}</span></div>`).join('')||'<div class="p-3 text-muted">No directories.</div>'; }catch(e){$('pathList').innerHTML=`<div class="text-danger p-2">${esc(e.message)}</div>`;} }
|
async function browsePath(path){ $('pathList').innerHTML='<span class="spinner-border spinner-border-sm"></span> Loading...'; try{ const res=await fetch(`/api/path/browse?path=${encodeURIComponent(path||'/')}`); const j=await res.json(); if(!j.ok) throw new Error(j.error); $('pathCurrent').value=j.path; lastPathParent=j.parent; $('pathList').innerHTML=j.dirs.map(d=>`<div class="path-row" data-path="${esc(d.path)}"><i class="fa-solid fa-folder"></i><span>${esc(d.name)}</span></div>`).join('')||'<div class="p-3 text-muted">No directories.</div>'; }catch(e){$('pathList').innerHTML=`<div class="text-danger p-2">${esc(e.message)}</div>`;} }
|
||||||
$('pathList')?.addEventListener('click',e=>{const r=e.target.closest('.path-row'); if(r) browsePath(r.dataset.path);}); $('pathGoBtn')?.addEventListener('click',()=>browsePath($('pathCurrent').value)); $('pathUpBtn')?.addEventListener('click',()=>browsePath(lastPathParent)); $('pathReloadBtn')?.addEventListener('click',()=>browsePath($('pathCurrent').value)); $('pathSelectBtn')?.addEventListener('click',async()=>{const p=$('pathCurrent').value; if(pathTarget==='move'){ const hashes=selectedHashes(); const j=await post('/api/torrents/move',{hashes,path:p,move_data:!!($('moveDataPhysical')?.checked),recheck:!!($('moveRecheck')?.checked)}); markTorrentOperation(hashes,'move',j.job_id,'queued'); toast($('moveDataPhysical')?.checked?'physical move queued':'move queued','success'); } else if($(pathTarget)) $(pathTarget).value=p; bootstrap.Modal.getInstance($('pathModal'))?.hide();}); document.querySelectorAll('.browse-path').forEach(b=>b.addEventListener('click',()=>openPathPicker(b.dataset.target)));
|
$('pathList')?.addEventListener('click',e=>{const r=e.target.closest('.path-row'); if(r) browsePath(r.dataset.path);}); $('pathGoBtn')?.addEventListener('click',()=>browsePath($('pathCurrent').value)); $('pathUpBtn')?.addEventListener('click',()=>browsePath(lastPathParent)); $('pathReloadBtn')?.addEventListener('click',()=>browsePath($('pathCurrent').value)); $('pathSelectBtn')?.addEventListener('click',async()=>{const p=$('pathCurrent').value; if(pathTarget==='move'){ const hashes=selectedHashes(); const j=await post('/api/torrents/move',{hashes,path:p,move_data:!!($('moveDataPhysical')?.checked),recheck:!!($('moveRecheck')?.checked)}); markTorrentOperation(hashes,'move',j.job_id,'queued'); toast($('moveDataPhysical')?.checked?'physical move queued':'move queued','success'); } else if($(pathTarget)) $(pathTarget).value=p; bootstrap.Modal.getInstance($('pathModal'))?.hide();}); document.querySelectorAll('.browse-path').forEach(b=>b.addEventListener('click',()=>openPathPicker(b.dataset.target)));
|
||||||
|
|
||||||
function renderColumnManager(){ const box=$('columnManager'); if(!box) return; box.innerHTML=COLUMN_DEFS.map(([key,label])=>`<label class="column-card ${hiddenColumns.has(key)?'':'active'}"><input class="form-check-input column-toggle" type="checkbox" data-col-key="${esc(key)}" ${hiddenColumns.has(key)?'':'checked'}><span><i class="fa-solid fa-table-columns"></i> ${esc(label)}</span></label>`).join(''); }
|
function renderColumnManager(){ const box=$('columnManager'); if(!box) return; box.innerHTML=COLUMN_DEFS.map(([key,label])=>`<label class="column-card form-check form-switch ${hiddenColumns.has(key)?'':'active'}"><input class="form-check-input column-toggle" type="checkbox" data-col-key="${esc(key)}" ${hiddenColumns.has(key)?'':'checked'}><span class="form-check-label"><i class="fa-solid fa-table-columns"></i> ${esc(label)}</span></label>`).join(''); }
|
||||||
$('saveColumnsBtn')?.addEventListener('click',async()=>{ document.querySelectorAll('.column-toggle').forEach(cb=>cb.checked?hiddenColumns.delete(cb.dataset.colKey):hiddenColumns.add(cb.dataset.colKey)); applyColumnVisibility(); scheduleRender(true); await post('/api/preferences',{table_columns_json:JSON.stringify({hidden:[...hiddenColumns]})}).catch(e=>toast(e.message,'danger')); toast('Columns saved','success'); });
|
$('saveColumnsBtn')?.addEventListener('click',async()=>{ document.querySelectorAll('.column-toggle').forEach(cb=>cb.checked?hiddenColumns.delete(cb.dataset.colKey):hiddenColumns.add(cb.dataset.colKey)); applyColumnVisibility(); scheduleRender(true); await post('/api/preferences',{table_columns_json:JSON.stringify({hidden:[...hiddenColumns]})}).catch(e=>toast(e.message,'danger')); toast('Columns saved','success'); });
|
||||||
$('resetColumnsBtn')?.addEventListener('click',async()=>{ hiddenColumns.clear(); renderColumnManager(); applyColumnVisibility(); scheduleRender(true); await post('/api/preferences',{table_columns_json:JSON.stringify({hidden:[]})}).catch(()=>{}); });
|
$('resetColumnsBtn')?.addEventListener('click',async()=>{ hiddenColumns.clear(); renderColumnManager(); applyColumnVisibility(); scheduleRender(true); await post('/api/preferences',{table_columns_json:JSON.stringify({hidden:[]})}).catch(()=>{}); });
|
||||||
|
|
||||||
@@ -336,7 +336,9 @@
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
function rtConfigInputValue(input){
|
function rtConfigInputValue(input){
|
||||||
return normalizeRtConfigValue(input.value, input.dataset.type || rtConfigFieldTypes.get(input.dataset.key) || 'text');
|
const type=input.dataset.type || rtConfigFieldTypes.get(input.dataset.key) || 'text';
|
||||||
|
const value=type==='bool' && input.type==='checkbox' ? (input.checked?'1':'0') : input.value;
|
||||||
|
return normalizeRtConfigValue(value, type);
|
||||||
}
|
}
|
||||||
function rtConfigOriginalValue(input){
|
function rtConfigOriginalValue(input){
|
||||||
const key=input.dataset.key;
|
const key=input.dataset.key;
|
||||||
@@ -402,7 +404,7 @@
|
|||||||
const valueNote=f.saved?`<small class="rt-config-value-note">Reference: ${esc(originalValue)} → saved: ${esc(displayValue)}</small>`:'';
|
const valueNote=f.saved?`<small class="rt-config-value-note">Reference: ${esc(originalValue)} → saved: ${esc(displayValue)}</small>`:'';
|
||||||
const originalAttr=esc(originalValue);
|
const originalAttr=esc(originalValue);
|
||||||
const input=type==='bool'
|
const input=type==='bool'
|
||||||
? `<select class="form-select form-select-sm rt-config-input" data-key="${esc(f.key)}" data-type="bool" data-original="${originalAttr}" data-saved="${f.saved?'true':'false'}" ${disabled}><option value="0" ${displayValue==='0'?'selected':''}>Off</option><option value="1" ${displayValue==='1'?'selected':''}>On</option></select>`
|
? `<span class="form-check form-switch rt-config-switch"><input class="form-check-input rt-config-input" data-key="${esc(f.key)}" data-type="bool" data-original="${originalAttr}" data-saved="${f.saved?'true':'false'}" type="checkbox" ${displayValue==='1'?'checked':''} ${disabled}><span class="form-check-label">${displayValue==='1'?'On':'Off'}</span></span>`
|
||||||
: `<input class="form-control form-control-sm rt-config-input" data-key="${esc(f.key)}" data-type="${esc(type)}" data-original="${originalAttr}" data-saved="${f.saved?'true':'false'}" type="${type==='number'?'number':'text'}" value="${esc(displayValue)}" placeholder="${esc(f.placeholder||'')}" ${disabled}>`;
|
: `<input class="form-control form-control-sm rt-config-input" data-key="${esc(f.key)}" data-type="${esc(type)}" data-original="${originalAttr}" data-saved="${f.saved?'true':'false'}" type="${type==='number'?'number':'text'}" value="${esc(displayValue)}" placeholder="${esc(f.placeholder||'')}" ${disabled}>`;
|
||||||
return `${head}<label class="rt-config-row ${f.ok?'':'disabled'} ${f.changed?'changed-live':''}"><span><b>${esc(f.label)}</b><small>${esc(f.key)}${note}</small>${valueNote}</span>${input}</label>`;
|
return `${head}<label class="rt-config-row ${f.ok?'':'disabled'} ${f.changed?'changed-live':''}"><span><b>${esc(f.label)}</b><small>${esc(f.key)}${note}</small>${valueNote}</span>${input}</label>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -491,9 +493,9 @@
|
|||||||
function portStatusLabel(st){ return st==='open'?'open':st==='closed'?'closed':st==='disabled'?'disabled':st==='error'?'error':'unknown'; }
|
function portStatusLabel(st){ return st==='open'?'open':st==='closed'?'closed':st==='disabled'?'disabled':st==='error'?'error':'unknown'; }
|
||||||
function portStatusClass(st){ return st==='open'?'port-ok':st==='closed'?'port-bad':'port-secondary'; }
|
function portStatusClass(st){ return st==='open'?'port-ok':st==='closed'?'port-bad':'port-secondary'; }
|
||||||
function portStatusIcon(st){ return st==='open'?'fa-circle-check':st==='closed'?'fa-circle-xmark':'fa-circle-question'; }
|
function portStatusIcon(st){ return st==='open'?'fa-circle-check':st==='closed'?'fa-circle-xmark':'fa-circle-question'; }
|
||||||
function portStatusBadge(data={},attrs='',withPort=false){ const st=portStatusLabel(data.status); const port=data.port?String(data.port):'-'; const label=withPort?`Port ${port} ${st}`:st; return `<span ${attrs}class="port-status ${portStatusClass(st)}"><i class="fa-solid ${portStatusIcon(st)}"></i> ${esc(label)}</span>`; }
|
function portStatusBadge(data={},attrs='',withPort=false){ const st=portStatusLabel(data.status); const active=data.open_port||data.port; const port=active?String(active):'-'; const label=withPort?`Port ${port} ${st}`:st; return `<span ${attrs}class="port-status ${portStatusClass(st)}"><i class="fa-solid ${portStatusIcon(st)}"></i> ${esc(label)}</span>`; }
|
||||||
function portCheckedAt(data={}){ if(data.checked_at) return String(data.checked_at).replace('T',' ').replace(/\+00:00$/,' UTC'); if(data.checked_at_epoch) return new Date(Number(data.checked_at_epoch)*1000).toLocaleString(); return ''; }
|
function portCheckedAt(data={}){ if(data.checked_at) return String(data.checked_at).replace('T',' ').replace(/\+00:00$/,' UTC'); if(data.checked_at_epoch) return new Date(Number(data.checked_at_epoch)*1000).toLocaleString(); return ''; }
|
||||||
function portCheckDetails(data={}){ const bits=[]; if(data.port) bits.push(`Port: ${data.port}`); if(data.public_ip) bits.push(`Public IP: ${data.public_ip}`); if(data.remote) bits.push('Remote profile'); if(data.source) bits.push(`Source: ${data.source}`); const checked=portCheckedAt(data); if(checked) bits.push(`Last check: ${checked}`); if(data.cached) bits.push('Cached result'); if(data.error) bits.push(data.error); if(data.fallback_error) bits.push(data.fallback_error); return bits; }
|
function portCheckDetails(data={}){ const bits=[]; if(data.open_port) bits.push(`Open port: ${data.open_port}`); else if(data.port) bits.push(`First port: ${data.port}`); if(Array.isArray(data.ports)&&data.ports.length>1) bits.push(`Candidates: ${data.ports.join(', ')}`); if(Array.isArray(data.checked_ports)&&data.checked_ports.length) bits.push(`Checked: ${data.checked_ports.join(', ')}`); if(data.ports_truncated) bits.push('Port list truncated to safety limit'); if(data.public_ip) bits.push(`Public IP: ${data.public_ip}`); if(data.remote) bits.push('Remote profile'); if(data.source) bits.push(`Source: ${data.source}`); const checked=portCheckedAt(data); if(checked) bits.push(`Last check: ${checked}`); if(data.cached) bits.push('Cached result'); if(data.error) bits.push(data.error); if(data.fallback_error) bits.push(data.fallback_error); return bits; }
|
||||||
function renderPortCheck(data={}){
|
function renderPortCheck(data={}){
|
||||||
if($('portCheckEnabled')) $('portCheckEnabled').checked=!!data.enabled;
|
if($('portCheckEnabled')) $('portCheckEnabled').checked=!!data.enabled;
|
||||||
const details=portCheckDetails(data);
|
const details=portCheckDetails(data);
|
||||||
@@ -531,7 +533,7 @@
|
|||||||
}catch(e){ box.innerHTML=`<div class="text-danger">${esc(e.message)}</div>`; }
|
}catch(e){ box.innerHTML=`<div class="text-danger">${esc(e.message)}</div>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
$('toolsModal')?.addEventListener('show.bs.modal',()=>{refreshProfiles();loadLabels();loadRatios();loadRss();loadSmartQueue();loadRtConfig();loadAutomations();loadCleanup();loadAppStatus();loadPreferences();renderColumnManager();applyColumnVisibility();updateAutomationForm();}); const toolPanelIds={rtorrents:'toolRtorrents',settings:'toolRtorrents',preferences:'toolPreferences',labels:'toolLabels',ratio:'toolRatio',rss:'toolRss',columns:'toolColumns',smart:'toolSmart',automations:'toolAutomations',rtconfig:'toolRtconfig',cleanup:'toolCleanup',appstatus:'toolAppstatus'}; const hideToolPanels=()=>Object.values(toolPanelIds).filter((v,i,a)=>a.indexOf(v)===i).forEach(id=>$(id)?.classList.add('d-none')); const showToolPanel=tool=>{hideToolPanels(); $(toolPanelIds[tool]||'toolRtorrents')?.classList.remove('d-none');}; const activateToolTab=tool=>{document.querySelectorAll('.tool-tab').forEach(x=>x.classList.toggle('active',(x.dataset.tool||'rtorrents')===tool)); showToolPanel(tool); if(tool==='appstatus') loadAppStatus(); if(tool==='cleanup') loadCleanup(); if(tool==='preferences') loadPreferences();}; document.querySelectorAll('.tool-tab').forEach(b=>b.addEventListener('click',()=>activateToolTab(b.dataset.tool||'rtorrents'))); $('rssFeedBtn')?.addEventListener('click',async()=>{await post('/api/rss/feeds',{name:$('rssName').value,url:$('rssUrl').value}); loadRss();}); $('rssRuleBtn')?.addEventListener('click',async()=>{await post('/api/rss/rules',{name:$('rssRuleName').value,pattern:$('rssPattern').value,save_path:$('rssPath').value,label:$('rssLabel').value}); loadRss();}); $('rssCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/rss/check',{}); toast(`RSS queued ${j.queued} item(s)`,'success');}catch(e){toast(e.message,'danger');} finally{setBusy(false);}}); $('smartSaveBtn')?.addEventListener('click',saveSmartQueue); $('smartCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/smart-queue/check',{}); const r=j.result||{}; toast(`Smart Queue: paused ${r.paused?.length||0}, resumed ${r.resumed?.length||0}`,'success'); await loadSmartQueue();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('smartManager')?.addEventListener('click',async e=>{const h=e.target.closest('.smart-unexclude')?.dataset.hash; if(!h)return; await post('/api/smart-queue/exclusion',{hash:h,excluded:false}); await loadSmartQueue();}); $('cleanupManager')?.addEventListener('click',async e=>{ if(e.target.closest('#cleanupRefreshBtn')) return loadCleanup(); if(e.target.closest('#cleanupJobsBtn')) return runCleanupAction('/api/cleanup/jobs','Clear finished job logs'); if(e.target.closest('#cleanupSmartQueueBtn')) return runCleanupAction('/api/cleanup/smart-queue','Clear Smart Queue logs'); if(e.target.closest('#cleanupAllBtn')) return runCleanupAction('/api/cleanup/all','Clear job and Smart Queue logs'); }); $('rtConfigReloadBtn')?.addEventListener('click',loadRtConfig); $('rtConfigSaveBtn')?.addEventListener('click',saveRtConfig); $('rtConfigGenerateBtn')?.addEventListener('click',generateRtConfig); $('rtConfigManager')?.addEventListener('input',e=>{ if(e.target.classList.contains('rt-config-input')) updateRtConfigDirty(); }); $('rtConfigManager')?.addEventListener('change',e=>{ if(e.target.classList.contains('rt-config-input')) updateRtConfigDirty(); }); $('rtConfigApplyOnStart')?.addEventListener('change',updateRtConfigDirty); $('peersRefreshSelect')?.addEventListener('change',async e=>{peersRefreshSeconds=Number(e.target.value||0); await post('/api/preferences',{peers_refresh_seconds:peersRefreshSeconds}).catch(()=>{}); setupPeersRefresh(activeTab()); toast('Peers refresh preference saved','success');});
|
$('toolsModal')?.addEventListener('show.bs.modal',()=>{refreshProfiles();loadLabels();loadRatios();loadRss();loadSmartQueue();loadRtConfig();loadAutomations();loadCleanup();loadAppStatus();loadPreferences();renderColumnManager();applyColumnVisibility();updateAutomationForm();}); const toolPanelIds={rtorrents:'toolRtorrents',settings:'toolRtorrents',preferences:'toolPreferences',labels:'toolLabels',ratio:'toolRatio',rss:'toolRss',columns:'toolColumns',smart:'toolSmart',automations:'toolAutomations',rtconfig:'toolRtconfig',cleanup:'toolCleanup',appstatus:'toolAppstatus'}; const hideToolPanels=()=>Object.values(toolPanelIds).filter((v,i,a)=>a.indexOf(v)===i).forEach(id=>$(id)?.classList.add('d-none')); const showToolPanel=tool=>{hideToolPanels(); $(toolPanelIds[tool]||'toolRtorrents')?.classList.remove('d-none');}; const activateToolTab=tool=>{document.querySelectorAll('.tool-tab').forEach(x=>x.classList.toggle('active',(x.dataset.tool||'rtorrents')===tool)); showToolPanel(tool); if(tool==='appstatus') loadAppStatus(); if(tool==='cleanup') loadCleanup(); if(tool==='preferences') loadPreferences();}; document.querySelectorAll('.tool-tab').forEach(b=>b.addEventListener('click',()=>activateToolTab(b.dataset.tool||'rtorrents'))); $('rssFeedBtn')?.addEventListener('click',async()=>{await post('/api/rss/feeds',{name:$('rssName').value,url:$('rssUrl').value}); loadRss();}); $('rssRuleBtn')?.addEventListener('click',async()=>{await post('/api/rss/rules',{name:$('rssRuleName').value,pattern:$('rssPattern').value,save_path:$('rssPath').value,label:$('rssLabel').value}); loadRss();}); $('rssCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/rss/check',{}); toast(`RSS queued ${j.queued} item(s)`,'success');}catch(e){toast(e.message,'danger');} finally{setBusy(false);}}); $('smartSaveBtn')?.addEventListener('click',saveSmartQueue); $('smartCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/smart-queue/check',{}); const r=j.result||{}; toast(`Smart Queue: paused ${r.paused?.length||0}, resumed ${r.resumed?.length||0}`,'success'); await loadSmartQueue();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('smartManager')?.addEventListener('click',async e=>{const h=e.target.closest('.smart-unexclude')?.dataset.hash; if(!h)return; await post('/api/smart-queue/exclusion',{hash:h,excluded:false}); await loadSmartQueue();}); $('cleanupManager')?.addEventListener('click',async e=>{ if(e.target.closest('#cleanupRefreshBtn')) return loadCleanup(); if(e.target.closest('#cleanupJobsBtn')) return runCleanupAction('/api/cleanup/jobs','Clear finished job logs'); if(e.target.closest('#cleanupSmartQueueBtn')) return runCleanupAction('/api/cleanup/smart-queue','Clear Smart Queue logs'); if(e.target.closest('#cleanupAllBtn')) return runCleanupAction('/api/cleanup/all','Clear job and Smart Queue logs'); }); $('rtConfigReloadBtn')?.addEventListener('click',loadRtConfig); $('rtConfigSaveBtn')?.addEventListener('click',saveRtConfig); $('rtConfigGenerateBtn')?.addEventListener('click',generateRtConfig); $('rtConfigManager')?.addEventListener('input',e=>{ if(e.target.classList.contains('rt-config-input')) updateRtConfigDirty(); }); $('rtConfigManager')?.addEventListener('change',e=>{ if(e.target.classList.contains('rt-config-input')){ const label=e.target.closest('.rt-config-switch')?.querySelector('.form-check-label'); if(label) label.textContent=e.target.checked?'On':'Off'; updateRtConfigDirty(); } }); $('rtConfigApplyOnStart')?.addEventListener('change',updateRtConfigDirty); $('peersRefreshSelect')?.addEventListener('change',async e=>{peersRefreshSeconds=Number(e.target.value||0); await post('/api/preferences',{peers_refresh_seconds:peersRefreshSeconds}).catch(()=>{}); setupPeersRefresh(activeTab()); toast('Peers refresh preference saved','success');});
|
||||||
$('autoConditionType')?.addEventListener('change',updateAutomationForm); $('autoEffectType')?.addEventListener('change',updateAutomationForm); $('automationSaveBtn')?.addEventListener('click',saveAutomation); $('automationCheckBtn')?.addEventListener('click',async()=>{setBusy(true);try{const j=await post('/api/automations/check',{}); toast(`Automations applied ${j.result?.applied?.length||0} item(s)`,'success'); await loadAutomations();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('automationManager')?.addEventListener('click',async e=>{const id=e.target.closest('.automation-delete')?.dataset.id;if(!id)return;if(!confirm('Delete this automation rule?'))return;const r=await fetch('/api/automations/'+id,{method:'DELETE'});const j=await r.json();if(!j.ok)toast(j.error||'Delete failed','danger');await loadAutomations();});
|
$('autoConditionType')?.addEventListener('change',updateAutomationForm); $('autoEffectType')?.addEventListener('change',updateAutomationForm); $('automationSaveBtn')?.addEventListener('click',saveAutomation); $('automationCheckBtn')?.addEventListener('click',async()=>{setBusy(true);try{const j=await post('/api/automations/check',{}); toast(`Automations applied ${j.result?.applied?.length||0} item(s)`,'success'); await loadAutomations();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('automationManager')?.addEventListener('click',async e=>{const id=e.target.closest('.automation-delete')?.dataset.id;if(!id)return;if(!confirm('Delete this automation rule?'))return;const r=await fetch('/api/automations/'+id,{method:'DELETE'});const j=await r.json();if(!j.ok)toast(j.error||'Delete failed','danger');await loadAutomations();});
|
||||||
document.addEventListener('click',async e=>{ const btn=e.target.closest('.delete-label'); if(!btn)return; if(!confirm('Delete this label?')) return; setBusy(true); try{ const r=await fetch('/api/labels/'+btn.dataset.id,{method:'DELETE'}); const j=await r.json(); if(!j.ok) throw new Error(j.error||'Delete failed'); await loadLabels(); toast('Label deleted','success'); }catch(err){toast(err.message,'danger');} finally{setBusy(false);} });
|
document.addEventListener('click',async e=>{ const btn=e.target.closest('.delete-label'); if(!btn)return; if(!confirm('Delete this label?')) return; setBusy(true); try{ const r=await fetch('/api/labels/'+btn.dataset.id,{method:'DELETE'}); const j=await r.json(); if(!j.ok) throw new Error(j.error||'Delete failed'); await loadLabels(); toast('Label deleted','success'); }catch(err){toast(err.message,'danger');} finally{setBusy(false);} });
|
||||||
$('bulkClearBtn')?.addEventListener('click',()=>{selected.clear(); selectedHash=null; lastSelectedHash=null; updateBulkBar(); if($('selectAll')) $('selectAll').checked=false; if($('detailPane')) $('detailPane').innerHTML='Select a torrent.'; setupPeersRefresh('general'); scheduleRender(true);});
|
$('bulkClearBtn')?.addEventListener('click',()=>{selected.clear(); selectedHash=null; lastSelectedHash=null; updateBulkBar(); if($('selectAll')) $('selectAll').checked=false; if($('detailPane')) $('detailPane').innerHTML='Select a torrent.'; setupPeersRefresh('general'); scheduleRender(true);});
|
||||||
|
|||||||
@@ -389,11 +389,53 @@ body.mobile-mode .torrent-table { display: none; }
|
|||||||
.column-check{padding:.35rem .5rem;border:1px solid var(--bs-border-color);border-radius:.5rem;background:var(--bs-body-bg)}
|
.column-check{padding:.35rem .5rem;border:1px solid var(--bs-border-color);border-radius:.5rem;background:var(--bs-body-bg)}
|
||||||
.label-filters .label-filter{font-size:.82rem;padding:.34rem .5rem;margin-bottom:.15rem}
|
.label-filters .label-filter{font-size:.82rem;padding:.34rem .5rem;margin-bottom:.15rem}
|
||||||
.label-filters .label-filter i{opacity:.75;margin-right:.25rem}
|
.label-filters .label-filter i{opacity:.75;margin-right:.25rem}
|
||||||
.column-manager{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:.55rem}
|
.column-manager {
|
||||||
.column-card{display:flex;align-items:center;gap:.5rem;padding:.55rem .65rem;border:1px solid var(--bs-border-color);border-radius:.7rem;background:rgba(var(--bs-secondary-bg-rgb),.45);cursor:pointer;user-select:none;transition:background .15s,border-color .15s,transform .15s}
|
display: grid;
|
||||||
.column-card:hover{border-color:var(--bs-primary);background:var(--bs-primary-bg-subtle)}
|
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||||||
.column-card.active{border-color:rgba(var(--bs-primary-rgb),.55);background:var(--bs-primary-bg-subtle)}
|
gap: .55rem;
|
||||||
.column-card input{margin:0}.column-card span{display:flex;gap:.45rem;align-items:center;font-weight:600}.column-card i{opacity:.72}
|
}
|
||||||
|
|
||||||
|
.column-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .55rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: .55rem .65rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: .7rem;
|
||||||
|
background: rgba(var(--bs-secondary-bg-rgb), .45);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background .15s, border-color .15s, transform .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card:hover,
|
||||||
|
.column-card.active {
|
||||||
|
background: var(--bs-primary-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card:hover {
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card.active {
|
||||||
|
border-color: rgba(var(--bs-primary-rgb), .55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card .form-check-input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card .form-check-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .45rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-card i {
|
||||||
|
opacity: .72;
|
||||||
|
}
|
||||||
.path-row::before{content:'\f07b';font-family:'Font Awesome 6 Free';font-weight:900;color:var(--bs-warning)}
|
.path-row::before{content:'\f07b';font-family:'Font Awesome 6 Free';font-weight:900;color:var(--bs-warning)}
|
||||||
body.mobile-mode #mobileList{min-height:0;height:100%;overflow:auto;display:block!important}
|
body.mobile-mode #mobileList{min-height:0;height:100%;overflow:auto;display:block!important}
|
||||||
body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{min-width:34px}
|
body.mobile-mode .mobile-card{display:block}.mobile-card .mobile-actions button{min-width:34px}
|
||||||
@@ -643,6 +685,22 @@ body.mobile-mode #mobileList { padding-top: 5.2rem !important; }
|
|||||||
background: rgba(var(--bs-secondary-bg-rgb), .35);
|
background: rgba(var(--bs-secondary-bg-rgb), .35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rt-config-switch {
|
||||||
|
justify-self: end;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rt-config-switch .form-check-input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rt-config-switch .form-check-label {
|
||||||
|
min-width: 2rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.rt-config-row b {
|
.rt-config-row b {
|
||||||
font-size: .88rem;
|
font-size: .88rem;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user