export const plannerSource = " function ensurePlannerToolsUI(){\n addToolTab('planner','fa-calendar-days','Planner','appstatus');\n addToolTab('poller','fa-satellite-dish','Poller','appstatus');\n const host=$('toolRss')?.parentElement || document.querySelector('#toolsModal .modal-body');\n if(!host) return;\n if(!$('toolPlanner')){\n const panel=document.createElement('div');\n panel.id='toolPlanner'; panel.className='d-none';\n panel.innerHTML=`
\n \n
\n
\n
\n
\n
\n
Download planner off
\n
${inlineSwitch('plannerEnabled')}
\n
\n
Current settingsLoading planner settings...
\n
\n
\n Basics\n
\n \n \n \n \n
\n
\n
\n Hourly speed planner\n ${plannerToggleRow('plannerHourlyEnabled','Use hourly speed limits','When enabled, the current hour overrides weekday and weekend speed limits.')}\n
\n
\n
\n
\n Fallback speed limits\n
${plannerSpeedCard('plannerWeekday','Weekday limits','Used when hourly planner is disabled')}${plannerSpeedCard('plannerWeekend','Weekend limits','Saturday and Sunday fallback')}
\n
\n
\n Time windows\n
\n ${plannerToggleRow('plannerNightOnly','Download only at night','Pause downloads outside the selected window.')}\n ${plannerToggleRow('plannerQuietEnabled','Quiet hours','Pause active downloads during the selected quiet window.')}\n
\n
\n \n \n \n \n
\n
\n
\n Protection\n
\n ${plannerToggleRow('plannerCpuEnabled','CPU protection','Pause downloads when CPU usage stays above the threshold for about 10 seconds.')}\n ${plannerToggleRow('plannerDiskEnabled','Disk protection','Pause downloads and block new download starts when disk usage is high.')}\n ${plannerToggleRow('plannerNetworkEnabled','Network protection','Clamp Planner speed limits to configured network caps.')}\n ${plannerToggleRow('plannerLoadEnabled','Load protection','Pause downloads when system load is above threshold.')}\n ${plannerToggleRow('plannerAutoResume','Auto resume planner-paused torrents','Resume only torrents paused by the planner when all protection rules become clear.')}\n
\n
\n \n \n \n \n \n
\n
\n
PreviewNo preview loaded.
\n
\n
\n
\n
\n
\n
\n
Action history
No actions yet.
\n
\n
\n
`\n host.appendChild(panel);\n renderPlannerHourlyGrid();\n // Note: Planner cards are collapsed by default; the summary bar keeps the active state visible.\n panel.addEventListener('change', e=>{ if(e.target.closest('#toolPlanner')) updatePlannerCurrentSummary(); });\n $('plannerSaveBtn')?.addEventListener('click',saveDownloadPlanner);\n $('plannerCheckBtn')?.addEventListener('click',()=>applyDownloadPlannerNow(false));\n $('plannerDryRunBtn')?.addEventListener('click',()=>applyDownloadPlannerNow(true));\n $('plannerOverrideBtn')?.addEventListener('click',setPlannerOverride);\n $('plannerPreviewBtn')?.addEventListener('click',loadPlannerPreview);\n $('plannerHistory')?.addEventListener('click',async e=>{\n const toggle=e.target.closest('#plannerHistoryToggle');\n const clear=e.target.closest('#plannerHistoryClear');\n if(toggle){ plannerHistoryExpanded=!plannerHistoryExpanded; await loadPlannerPreview(); return; }\n if(clear && confirm('Clear Planner action history?')){\n try{ await post('/api/download-planner/history',{},'DELETE'); plannerHistoryExpanded=false; await loadPlannerPreview(); toast('Planner history cleared','success'); }\n catch(err){ toast(err.message,'danger'); }\n }\n });\n $('plannerProfileName')?.addEventListener('change',applyPlannerPreset);\n $('plannerHourCopyWeekday')?.addEventListener('click',()=>copyPlannerSpeedToHours('plannerWeekday'));\n document.querySelectorAll('.planner-hour-fill').forEach(btn=>btn.addEventListener('click',()=>fillPlannerHours(Number(btn.dataset.mbps||0))));\n setupPlannerSpeedControls();\n }\n if(!$('toolPoller')){\n const panel=document.createElement('div');\n panel.id='toolPoller'; panel.className='d-none';\n panel.innerHTML=`
\n
\n
Smart poller normal
Controls separate lightweight live polling and slower full torrent-list polling per active rTorrent profile.
\n
${inlineSwitch('pollerAdaptive')}
\n
\n
\n
\n Adaptive behavior\n
These values decide when the poller slows down, recovers after errors, or switches between active and idle refreshes.
\n
\n \n \n \n \n \n \n
\n
\n
\n Live poller\n
Fast lightweight loop for volatile data: speeds, torrent status changes and UI heartbeat. It should stay responsive, but not below the safe baseline on busy rTorrent instances.
\n
\n \n \n \n
\n
\n
\n Full poller\n
Slower loop for heavier work: full torrent snapshot/diff, tracker summary, disk state, queue, jobs and planner checks.
\n
\n \n \n \n \n
\n
\n ${plannerToggleRow('pollerSafeFallback','Safe fallback mode','When enabled, unsafe intervals are raised before saving. It protects rTorrent and the browser from too-aggressive refresh loops while keeping values that are already safe unchanged.')}\n
Enable Safe fallback mode to protect the app from too-aggressive poller intervals.
\n
DiagnosticsNot loaded.
\n
\n
\n
`;\n host.appendChild(panel);\n $('pollerSaveBtn')?.addEventListener('click',savePollerSettings);\n $('pollerReloadBtn')?.addEventListener('click',loadPollerSettings);\n document.querySelectorAll('#toolPoller input').forEach(input=>input.addEventListener('input',()=>{ if(typeof updatePollerSafeFallbackPreview==='function') updatePollerSafeFallbackPreview(); }));\n document.querySelectorAll('#toolPoller input').forEach(input=>input.addEventListener('change',()=>{ if(typeof updatePollerSafeFallbackPreview==='function') updatePollerSafeFallbackPreview(); }));\n if(typeof updatePollerSafeFallbackPreview==='function') updatePollerSafeFallbackPreview();\n }\n }\n const plannerMbpsToBytes=mbps=>mbps?Math.round(Number(mbps)*1000000/8):0;\n const plannerBytesToMbps=bytes=>bytes?Math.round(Number(bytes)*8/1000000):0;\n function plannerLimitText(bytes){ const mbps=plannerBytesToMbps(Number(bytes||0)); return mbps?`${mbps} Mbit/s`:'Unlimited'; }\n function plannerHourLabel(hour){ return `${String(hour).padStart(2,'0')}:00-${String((hour+1)%24).padStart(2,'0')}:00`; }\n function renderPlannerHourlyGrid(){\n const box=$('plannerHourlyGrid'); if(!box) return;\n box.innerHTML=Array.from({length:24},(_,hour)=>`
${plannerHourLabel(hour)}Unlimited
`).join('');\n document.querySelectorAll('.planner-hour-input').forEach(input=>input.addEventListener('input',()=>updatePlannerHourSummary(Number(input.closest('.planner-hour-row')?.dataset.hour||0))));\n }\n function updatePlannerHourSummary(hour){ const down=Number($(`plannerHour${hour}Down`)?.value||0), up=Number($(`plannerHour${hour}Up`)?.value||0); const out=$(`plannerHour${hour}Summary`); if(out) out.textContent=`DL ${plannerLimitText(down)} / UL ${plannerLimitText(up)}`; }\n function fillPlannerHours(mbps){ const bytes=plannerMbpsToBytes(mbps); for(let hour=0;hour<24;hour++){ const d=$(`plannerHour${hour}Down`), u=$(`plannerHour${hour}Up`); if(d)d.value=bytes; if(u)u.value=bytes; updatePlannerHourSummary(hour); } }\n function copyPlannerSpeedToHours(prefix){ const down=Number($(`${prefix}Down`)?.value||0), up=Number($(`${prefix}Up`)?.value||0); for(let hour=0;hour<24;hour++){ const d=$(`plannerHour${hour}Down`), u=$(`plannerHour${hour}Up`); if(d)d.value=down; if(u)u.value=up; updatePlannerHourSummary(hour); } }\n function plannerHourlyPayload(){ return Array.from({length:24},(_,hour)=>({hour,down:Number($(`plannerHour${hour}Down`)?.value||0),up:Number($(`plannerHour${hour}Up`)?.value||0)})); }\n function setPlannerSpeed(prefix,mbps){\n const bytes=plannerMbpsToBytes(mbps);\n ['Down','Up'].forEach(dir=>{ const input=$(`${prefix}${dir}`); if(input) input.value=bytes; });\n updatePlannerSpeedControls(prefix);\n }\n function updatePlannerSpeedControls(prefix){\n const down=Number($(`${prefix}Down`)?.value||0), up=Number($(`${prefix}Up`)?.value||0);\n [['Down',down],['Up',up]].forEach(([dir,value])=>{ const slider=$(`${prefix}${dir}Slider`), out=$(`${prefix}${dir}Mbps`); const mbps=plannerBytesToMbps(value); if(slider){ if(mbps>Number(slider.max||0)) slider.max=String(mbps); slider.value=String(mbps); } if(out) out.textContent=plannerLimitText(value); });\n const sum=$(`${prefix}Summary`); if(sum) sum.textContent=`DL ${plannerLimitText(down)} / UL ${plannerLimitText(up)}`;\n }\n function setupPlannerSpeedControls(){\n document.querySelectorAll('.planner-speed-preset').forEach(btn=>btn.addEventListener('click',()=>setPlannerSpeed(btn.dataset.prefix,Number(btn.dataset.mbps||0))));\n document.querySelectorAll('.planner-mbps-slider').forEach(slider=>slider.addEventListener('input',()=>{ const target=$(slider.dataset.target); if(target) target.value=plannerMbpsToBytes(Number(slider.value||0)); const prefix=(slider.dataset.target||'').replace(/(Down|Up)$/,''); updatePlannerSpeedControls(prefix); }));\n document.querySelectorAll('.planner-byte-input').forEach(input=>input.addEventListener('input',()=>updatePlannerSpeedControls(input.id.replace(/(Down|Up)$/,''))));\n }\n function plannerPayload(){ return {enabled:$('plannerEnabled')?.checked,profile_name:$('plannerProfileName')?.value||'night mode',dry_run:$('plannerDryRun')?.checked,night_only_enabled:$('plannerNightOnly')?.checked,night_start:$('plannerNightStart')?.value||'23:00',night_end:$('plannerNightEnd')?.value||'07:00',quiet_hours_enabled:$('plannerQuietEnabled')?.checked,quiet_start:$('plannerQuietStart')?.value||'22:00',quiet_end:$('plannerQuietEnd')?.value||'06:00',weekday_down:Number($('plannerWeekdayDown')?.value||0),weekday_up:Number($('plannerWeekdayUp')?.value||0),weekend_down:Number($('plannerWeekendDown')?.value||0),weekend_up:Number($('plannerWeekendUp')?.value||0),hourly_schedule_enabled:$('plannerHourlyEnabled')?.checked,hourly_schedule:plannerHourlyPayload(),auto_pause_cpu_enabled:$('plannerCpuEnabled')?.checked,auto_pause_cpu_percent:Number($('plannerCpuPercent')?.value||90),auto_pause_disk_enabled:$('plannerDiskEnabled')?.checked,auto_pause_disk_percent:Number($('plannerDiskPercent')?.value||95),network_protection_enabled:$('plannerNetworkEnabled')?.checked,network_max_down:Number($('plannerNetworkDown')?.value||0),network_max_up:Number($('plannerNetworkUp')?.value||0),load_protection_enabled:$('plannerLoadEnabled')?.checked,load_cpu_percent:Number($('plannerLoadCpu')?.value||95),auto_resume:$('plannerAutoResume')?.checked,auto_resume_grace_seconds:Number($('plannerResumeGrace')?.value||0)}; }\n function plannerOnOff(value){ return value ? 'on' : 'off'; }\n function plannerSummaryValue(label, value){\n return `${esc(label)}: ${esc(value)}`;\n }\n\n // Note: Current Settings intentionally reuses the Poller Diagnostics row structure for matching radius, spacing and typography.\n function updatePlannerCurrentSummary(state={}){\n const box=$('plannerCurrentSummary');\n if(!box) return;\n const enabled=$('plannerEnabled')?.checked ?? !!state.enabled;\n const dryRun=$('plannerDryRun')?.checked;\n const nightStart=$('plannerNightStart')?.value || state.night_start || '--:--';\n const nightEnd=$('plannerNightEnd')?.value || state.night_end || '--:--';\n const quietStart=$('plannerQuietStart')?.value || state.quiet_start || '--:--';\n const quietEnd=$('plannerQuietEnd')?.value || state.quiet_end || '--:--';\n const items=[\n plannerSummaryValue('Status', `${enabled ? 'on' : 'off'}${dryRun ? ' / dry-run' : ''}`),\n plannerSummaryValue('Profile', $('plannerProfileName')?.value || state.profile_name || '-'),\n plannerSummaryValue('Hourly', plannerOnOff($('plannerHourlyEnabled')?.checked)),\n plannerSummaryValue('Night', `${plannerOnOff($('plannerNightOnly')?.checked)} ${nightStart}-${nightEnd}`),\n plannerSummaryValue('Quiet', `${plannerOnOff($('plannerQuietEnabled')?.checked)} ${quietStart}-${quietEnd}`),\n plannerSummaryValue('Protection', `CPU ${plannerOnOff($('plannerCpuEnabled')?.checked)}, disk ${plannerOnOff($('plannerDiskEnabled')?.checked)}, network ${plannerOnOff($('plannerNetworkEnabled')?.checked)}, load ${plannerOnOff($('plannerLoadEnabled')?.checked)}`),\n ];\n box.innerHTML=`
Current settings${items.join('')}
`;\n }\n\n function updatePlannerFooter(enabled,preview={}){ updatePlannerCurrentSummary(preview); const btn=$('statusPlannerOpen'); if(btn){ btn.classList.toggle('d-none',!enabled); btn.classList.toggle('text-warning',!!preview.manual_override_until); btn.title=enabled?`Planner ${preview.matched_rule||'enabled'}${preview.dry_run?' \u00b7 dry-run':''}`:'Download planner is disabled.'; const span=btn.querySelector('span'); if(span) span.textContent=preview.dry_run?'Planner dry-run':preview.manual_override_until?'Planner paused':'Planner'; } const badge=$('plannerStatusBadge'); if(badge){ badge.className=`badge ${enabled?'text-bg-success':'text-bg-secondary'}`; badge.textContent=enabled?(preview.dry_run?'dry-run':preview.manual_override_until?'override':'enabled'):'off'; } }\n function plannerDateText(value){ if(!value) return '-'; if(typeof value==='number') return formatDateTime(value); const d=new Date(value); return isNaN(d.getTime())?'-':d.toLocaleString(); }\n function renderPlannerPreview(preview={}){ updatePlannerCurrentSummary(preview); const box=$('plannerPreview'); if(!box)return; const down=plannerLimitText(preview.down||0), up=plannerLimitText(preview.up||0); box.innerHTML=`Matched ${esc(preview.matched_rule||'-')} \u00b7 next change ${esc(plannerDateText(preview.next_change_at))} \u00b7 DL ${esc(down)} / UL ${esc(up)}${preview.pause_downloads?' \u00b7 pauses downloads':''}${preview.manual_override_until?' \u00b7 override active':''}`; updatePlannerFooter(!!$('plannerEnabled')?.checked,preview); const ov=$('plannerOverrideStatus'); if(ov) ov.textContent=preview.manual_override_until?`Active until ${plannerDateText(preview.manual_override_until)}`:'No active override.'; }\n function plannerHistoryDetails(row={}){ return row && typeof row==='object' ? row : {}; }\n function plannerHistoryLimitText(value){ return plannerLimitText(Number(value||0)); }\n function renderPlannerHistory(items=[], total=items.length){\n const box=$('plannerHistory'); if(!box)return;\n const body=items.length\n ? responsiveTable(['Time','Event','Rule','DL','UL','Paused','Resumed','Dry run','Reason'],items.map(x=>{\n // Note: Planner history uses the same table pattern as Smart Queue, with compact decision columns first.\n const d=plannerHistoryDetails(x);\n const event=d.event||'-';\n const rule=d.rule||d.matched_rule||d.profile_name||'-';\n const down=d.down!==undefined?plannerHistoryLimitText(d.down):'-';\n const up=d.up!==undefined?plannerHistoryLimitText(d.up):'-';\n const paused=d.paused ?? d.count ?? 0;\n const resumed=d.resumed ?? 0;\n const dry=d.dry_run?'yes':'-';\n const reason=d.pause_reason||d.reason||d.manual_override_reason||'-';\n return [dateCell(d.at),esc(event),esc(rule),esc(down),esc(up),esc(paused),esc(resumed),esc(dry),esc(reason)];\n }),'planner-history-table')\n : '
No Planner actions yet.
';\n const canToggle=Number(total||0)>10;\n const toggle=canToggle?``:'';\n const clear=Number(total||0)?``:'';\n box.innerHTML=`${body}${toggle}${clear}`;\n }\n function fillPlanner(st){ if(!st)return; $('plannerEnabled')&&($('plannerEnabled').checked=!!st.enabled); $('plannerProfileName')&&($('plannerProfileName').value=st.profile_name||'night mode'); $('plannerDryRun')&&($('plannerDryRun').checked=!!st.dry_run); updatePlannerFooter(!!st.enabled,st); $('plannerHourlyEnabled')&&($('plannerHourlyEnabled').checked=!!st.hourly_schedule_enabled); const hourly=Array.isArray(st.hourly_schedule)?st.hourly_schedule:[]; for(let hour=0;hour<24;hour++){ const item=hourly.find(x=>Number(x.hour)===hour)||{}; const d=$(`plannerHour${hour}Down`), u=$(`plannerHour${hour}Up`); if(d)d.value=Number(item.down||0); if(u)u.value=Number(item.up||0); updatePlannerHourSummary(hour); } $('plannerNightOnly')&&($('plannerNightOnly').checked=!!st.night_only_enabled); $('plannerNightStart')&&($('plannerNightStart').value=st.night_start||'23:00'); $('plannerNightEnd')&&($('plannerNightEnd').value=st.night_end||'07:00'); $('plannerQuietEnabled')&&($('plannerQuietEnabled').checked=!!st.quiet_hours_enabled); $('plannerQuietStart')&&($('plannerQuietStart').value=st.quiet_start||'22:00'); $('plannerQuietEnd')&&($('plannerQuietEnd').value=st.quiet_end||'06:00'); $('plannerWeekdayDown')&&($('plannerWeekdayDown').value=st.weekday_down||0); $('plannerWeekdayUp')&&($('plannerWeekdayUp').value=st.weekday_up||0); $('plannerWeekendDown')&&($('plannerWeekendDown').value=st.weekend_down||0); $('plannerWeekendUp')&&($('plannerWeekendUp').value=st.weekend_up||0); updatePlannerSpeedControls('plannerWeekday'); updatePlannerSpeedControls('plannerWeekend'); $('plannerCpuEnabled')&&($('plannerCpuEnabled').checked=!!st.auto_pause_cpu_enabled); $('plannerCpuPercent')&&($('plannerCpuPercent').value=st.auto_pause_cpu_percent||90); $('plannerDiskEnabled')&&($('plannerDiskEnabled').checked=!!st.auto_pause_disk_enabled); $('plannerDiskPercent')&&($('plannerDiskPercent').value=st.auto_pause_disk_percent||95); $('plannerNetworkEnabled')&&($('plannerNetworkEnabled').checked=!!st.network_protection_enabled); $('plannerNetworkDown')&&($('plannerNetworkDown').value=st.network_max_down||0); $('plannerNetworkUp')&&($('plannerNetworkUp').value=st.network_max_up||0); $('plannerLoadEnabled')&&($('plannerLoadEnabled').checked=!!st.load_protection_enabled); $('plannerLoadCpu')&&($('plannerLoadCpu').value=st.load_cpu_percent||95); $('plannerAutoResume')&&($('plannerAutoResume').checked=st.auto_resume!==false); $('plannerResumeGrace')&&($('plannerResumeGrace').value=st.auto_resume_grace_seconds||0); if(st.manual_override_until) renderPlannerPreview(st); updatePlannerCurrentSummary(st); }\n function applyPlannerPreset(){ const name=$('plannerProfileName')?.value||''; if(name==='night mode'){ $('plannerNightOnly').checked=true; $('plannerQuietEnabled').checked=false; setPlannerSpeed('plannerWeekday',100); setPlannerSpeed('plannerWeekend',250); } if(name==='weekend mode'){ $('plannerNightOnly').checked=false; setPlannerSpeed('plannerWeekday',50); setPlannerSpeed('plannerWeekend',0); } if(name==='low power mode'){ $('plannerLoadEnabled').checked=true; $('plannerCpuEnabled').checked=true; $('plannerCpuPercent').value=70; setPlannerSpeed('plannerWeekday',50); setPlannerSpeed('plannerWeekend',50); } if(name==='unlimited mode'){ $('plannerNightOnly').checked=false; $('plannerQuietEnabled').checked=false; setPlannerSpeed('plannerWeekday',0); setPlannerSpeed('plannerWeekend',0); } }\n async function loadPlannerPreview(){ try{const limit=plannerHistoryExpanded?100:10; const j=await fetch(`/api/download-planner/preview?history_limit=${limit}`).then(r=>r.json()); renderPlannerPreview(j.preview||{}); renderPlannerHistory(j.history||[], Number(j.history_total ?? (j.history||[]).length));}catch(e){} }\n async function loadDownloadPlanner(){ ensurePlannerToolsUI(); try{const j=await fetch('/api/download-planner').then(r=>r.json()); fillPlanner(j.settings||{}); await loadPlannerPreview();}catch(e){} }\n async function saveDownloadPlanner(){ try{const j=await post('/api/download-planner',plannerPayload()); fillPlanner(j.settings||plannerPayload()); await loadPlannerPreview(); toast('Download planner saved','success');}catch(e){toast(e.message,'danger');} }\n async function applyDownloadPlannerNow(dryRun=false){ try{const j=await post('/api/download-planner/check',{dry_run:!!dryRun}); const r=j.result||{}; if(r.settings) fillPlanner(r.settings); renderPlannerPreview(r.preview||r); if(r.history) renderPlannerHistory(r.history, r.history_total ?? r.history.length); else await loadPlannerPreview(); toastMessage('toast.plannerApplied','success',{dryRun,paused:r.paused,resumed:r.resumed,limitsChanged:r.limits_changed});}catch(e){toast(e.message,'danger');} }\n async function setPlannerOverride(){ try{const seconds=Number($('plannerOverrideSeconds')?.value||0); await post('/api/download-planner/override',{seconds}); toast(seconds?'Planner override set':'Planner override cleared','success'); await loadDownloadPlanner();}catch(e){toast(e.message,'danger');} }\n";