2 lines
32 KiB
JavaScript
2 lines
32 KiB
JavaScript
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=`<div class=\"column-manager-tabs mb-3\">\n <ul class=\"nav nav-pills\" role=\"tablist\">\n <li class=\"nav-item\"><button class=\"nav-link active\" data-bs-toggle=\"pill\" data-bs-target=\"#plannerPane-settings\" type=\"button\" role=\"tab\"><i class=\"fa-solid fa-sliders\"></i> Settings</button></li>\n <li class=\"nav-item\"><button class=\"nav-link\" data-bs-toggle=\"pill\" data-bs-target=\"#plannerPane-history\" type=\"button\" role=\"tab\"><i class=\"fa-solid fa-clock-rotate-left\"></i> Action history</button></li>\n </ul>\n </div>\n <div class=\"tab-content\">\n <div id=\"plannerPane-settings\" class=\"tab-pane fade show active\" role=\"tabpanel\">\n <div class=\"surface-section smart-panel planner-panel\">\n <div class=\"smart-header planner-hero\">\n <div class=\"section-title\"><i class=\"fa-solid fa-calendar-days\"></i> Download planner <span id=\"plannerStatusBadge\" class=\"badge text-bg-secondary\">off</span></div>\n <div class=\"smart-header-actions\">${inlineSwitch('plannerEnabled')}</div>\n </div>\n <div id=\"plannerCurrentSummary\" class=\"smart-setting-row planner-current-summary mb-3\"><div><b><i class=\"fa-solid fa-sliders\"></i> Current settings</b><small class=\"planner-diagnostic-line\">Loading planner settings...</small></div></div>\n <div class=\"planner-layout\">\n <details class=\"planner-card planner-card-primary planner-disclosure\" open>\n <summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-sliders\"></i> Basics</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary>\n <div class=\"smart-input-grid planner-profile-grid\">\n <label class=\"smart-input-field\"><span>Planner profile</span><select id=\"plannerProfileName\" class=\"form-select form-select-sm\"><option value=\"night mode\">night mode</option><option value=\"weekend mode\">weekend mode</option><option value=\"low power mode\">low power mode</option><option value=\"unlimited mode\">unlimited mode</option></select><small>Saved per active rTorrent profile.</small></label>\n <label class=\"smart-input-field\"><span>Auto-resume grace s</span><input id=\"plannerResumeGrace\" class=\"form-control form-control-sm\" type=\"number\" min=\"0\" step=\"30\"><small>Wait before resuming planner-paused torrents.</small></label>\n <label class=\"smart-input-field\"><span>Manual override</span><select id=\"plannerOverrideSeconds\" class=\"form-select form-select-sm\"><option value=\"0\">clear override</option><option value=\"1800\">disable for 30 min</option><option value=\"3600\">disable for 1 hour</option><option value=\"7200\">disable for 2 hours</option></select><small id=\"plannerOverrideStatus\">No active override.</small></label>\n <label class=\"smart-input-field\"><span>Mode</span>${inlineSwitch('plannerDryRun')}<small>Dry-run previews actions without changing rTorrent.</small></label>\n </div>\n </details>\n <details class=\"planner-card planner-disclosure\">\n <summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-clock\"></i> Hourly speed planner</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary>\n ${plannerToggleRow('plannerHourlyEnabled','Use hourly speed limits','When enabled, the current hour overrides weekday and weekend speed limits.')}\n <div class=\"planner-hour-tools\"><button class=\"btn btn-xs btn-outline-secondary planner-hour-fill\" data-mbps=\"0\">Unlimited all day</button><button class=\"btn btn-xs btn-outline-secondary planner-hour-fill\" data-mbps=\"50\">50 Mbit/s all day</button><button class=\"btn btn-xs btn-outline-secondary planner-hour-fill\" data-mbps=\"100\">100 Mbit/s all day</button><button class=\"btn btn-xs btn-outline-primary\" id=\"plannerHourCopyWeekday\" type=\"button\">Copy weekday limit</button></div>\n <div id=\"plannerHourlyGrid\" class=\"planner-hour-grid\"></div>\n </details>\n <details class=\"planner-card planner-disclosure\">\n <summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-calendar-week\"></i> Fallback speed limits</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary>\n <div class=\"smart-input-grid planner-speed-grid\">${plannerSpeedCard('plannerWeekday','Weekday limits','Used when hourly planner is disabled')}${plannerSpeedCard('plannerWeekend','Weekend limits','Saturday and Sunday fallback')}</div>\n </details>\n <details class=\"planner-card planner-card-time planner-disclosure\">\n <summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-moon\"></i> Time windows</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary>\n <div class=\"planner-toggle-stack planner-toggle-stack-compact\">\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 </div>\n <div class=\"smart-input-grid planner-time-grid\">\n <label class=\"smart-input-field\"><span>Night start</span><input id=\"plannerNightStart\" class=\"form-control form-control-sm\" type=\"time\"></label>\n <label class=\"smart-input-field\"><span>Night end</span><input id=\"plannerNightEnd\" class=\"form-control form-control-sm\" type=\"time\"></label>\n <label class=\"smart-input-field\"><span>Quiet start</span><input id=\"plannerQuietStart\" class=\"form-control form-control-sm\" type=\"time\"><small>Used only when quiet hours are enabled.</small></label>\n <label class=\"smart-input-field\"><span>Quiet end</span><input id=\"plannerQuietEnd\" class=\"form-control form-control-sm\" type=\"time\"><small>Downloads pause and DL limit is set to 0.</small></label>\n </div>\n </details>\n <details class=\"planner-card planner-card-protection planner-disclosure\">\n <summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-shield-halved\"></i> Protection</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary>\n <div class=\"planner-toggle-stack planner-protection-toggles\">\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 </div>\n <div class=\"smart-input-grid planner-protection-grid\">\n <label class=\"smart-input-field\"><span>CPU threshold %</span><input id=\"plannerCpuPercent\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" max=\"100\"></label>\n <label class=\"smart-input-field\"><span>Disk threshold %</span><input id=\"plannerDiskPercent\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" max=\"100\"><small>Uses the disk source configured in Preferences.</small></label>\n <label class=\"smart-input-field\"><span>Network max DL B/s</span><input id=\"plannerNetworkDown\" class=\"form-control form-control-sm\" type=\"number\" min=\"0\"><small>0 keeps scheduled limit.</small></label>\n <label class=\"smart-input-field\"><span>Network max UL B/s</span><input id=\"plannerNetworkUp\" class=\"form-control form-control-sm\" type=\"number\" min=\"0\"><small>Optional load/network cap.</small></label>\n <label class=\"smart-input-field\"><span>Load CPU %</span><input id=\"plannerLoadCpu\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" max=\"100\"><small>Network/load protection trigger.</small></label>\n </div>\n </details>\n <details class=\"planner-card planner-card-result planner-disclosure\" open><summary class=\"planner-card-title\"><span><i class=\"fa-solid fa-eye\"></i> Preview</span><i class=\"fa-solid fa-chevron-down planner-card-chevron\"></i></summary><small id=\"plannerPreview\">No preview loaded.</small></details>\n </div>\n <div class=\"tool-action-row planner-actions\"><button id=\"plannerSaveBtn\" class=\"btn btn-primary btn-sm\"><i class=\"fa-solid fa-floppy-disk\"></i> Save planner</button><button id=\"plannerCheckBtn\" class=\"btn btn-success btn-sm\"><i class=\"fa-solid fa-play\"></i> Apply now</button><button id=\"plannerDryRunBtn\" class=\"btn btn-outline-secondary btn-sm\"><i class=\"fa-solid fa-vial\"></i> Dry-run now</button><button id=\"plannerOverrideBtn\" class=\"btn btn-outline-warning btn-sm\"><i class=\"fa-solid fa-pause\"></i> Set override</button><button id=\"plannerPreviewBtn\" class=\"btn btn-outline-secondary btn-sm\"><i class=\"fa-solid fa-eye\"></i> Refresh preview</button></div>\n </div>\n </div>\n <div id=\"plannerPane-history\" class=\"tab-pane fade\" role=\"tabpanel\">\n <div class=\"surface-section smart-panel planner-panel\">\n <div class=\"section-title\"><i class=\"fa-solid fa-clock-rotate-left\"></i> Action history</div><div id=\"plannerHistory\" class=\"mt-2\">No actions yet.</div>\n </div>\n </div>\n </div>`\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=`<div class=\"surface-section smart-panel poller-panel\">\n <div class=\"smart-header\">\n <div><div class=\"section-title\"><i class=\"fa-solid fa-satellite-dish\"></i> Adaptive WebSocket poller <span id=\"pollerStatusBadge\" class=\"badge text-bg-secondary\">normal</span></div><div class=\"tool-note\">Controls live refresh cadence per active rTorrent profile.</div></div>\n <div class=\"smart-header-actions\">${inlineSwitch('pollerAdaptive')}</div>\n </div>\n <div class=\"smart-settings-list\">\n <div class=\"smart-input-grid poller-input-grid\">\n <label class=\"smart-input-field\"><span>Active interval s</span><input id=\"pollerActive\" class=\"form-control form-control-sm\" type=\"number\" min=\"0.5\" step=\"0.5\"><small>Fast loop while torrents are active.</small></label>\n <label class=\"smart-input-field\"><span>Idle interval s</span><input id=\"pollerIdle\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" step=\"1\"><small>Slower loop when idle.</small></label>\n <label class=\"smart-input-field\"><span>Error interval s</span><input id=\"pollerError\" class=\"form-control form-control-sm\" type=\"number\" min=\"2\" step=\"1\"><small>Delay after rTorrent errors.</small></label>\n <label class=\"smart-input-field\"><span>Torrent list s</span><input id=\"pollerTorrentList\" class=\"form-control form-control-sm\" type=\"number\" min=\"0.5\" step=\"0.5\"><small>Main torrent diff loop.</small></label>\n <label class=\"smart-input-field\"><span>System stats s</span><input id=\"pollerSystem\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" step=\"1\"><small>CPU/RAM/speed stats cadence.</small></label>\n <label class=\"smart-input-field\"><span>Tracker stats s</span><input id=\"pollerTracker\" class=\"form-control form-control-sm\" type=\"number\" min=\"10\" step=\"5\"><small>Tracker summary cadence.</small></label>\n <label class=\"smart-input-field\"><span>Disk stats s</span><input id=\"pollerDisk\" class=\"form-control form-control-sm\" type=\"number\" min=\"5\" step=\"5\"><small>Disk usage cadence.</small></label>\n <label class=\"smart-input-field\"><span>Queue/job stats s</span><input id=\"pollerQueue\" class=\"form-control form-control-sm\" type=\"number\" min=\"5\" step=\"5\"><small>Smart Queue, jobs and planner cadence.</small></label>\n <label class=\"smart-input-field\"><span>Heartbeat s</span><input id=\"pollerHeartbeat\" class=\"form-control form-control-sm\" type=\"number\" min=\"2\" step=\"1\"><small>Minimum heartbeat spacing when no data changed.</small></label>\n <label class=\"smart-input-field\"><span>Slow threshold ms</span><input id=\"pollerSlowThreshold\" class=\"form-control form-control-sm\" type=\"number\" min=\"100\" step=\"100\"><small>Auto slowdown trigger.</small></label>\n <label class=\"smart-input-field\"><span>Slowdown multiplier</span><input id=\"pollerSlowdown\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" step=\"0.5\"><small>Adaptive backoff factor.</small></label>\n <label class=\"smart-input-field\"><span>Recovery errors</span><input id=\"pollerRecoveryErrors\" class=\"form-control form-control-sm\" type=\"number\" min=\"1\" step=\"1\"><small>Repeated errors before recovery mode.</small></label>\n </div>\n ${plannerToggleRow('pollerSafeFallback','Safe fallback mode','Clamp unsafe poller settings to known-safe intervals.')}\n <div class=\"smart-setting-row\"><div><b>Diagnostics</b><small id=\"pollerRuntime\">Not loaded.</small></div></div>\n </div>\n <div class=\"tool-action-row planner-actions\"><button id=\"pollerSaveBtn\" class=\"btn btn-primary btn-sm\"><i class=\"fa-solid fa-floppy-disk\"></i> Save poller</button><button id=\"pollerReloadBtn\" class=\"btn btn-outline-secondary btn-sm\"><i class=\"fa-solid fa-rotate\"></i> Reload</button></div>\n </div>`;\n host.appendChild(panel);\n $('pollerSaveBtn')?.addEventListener('click',savePollerSettings);\n $('pollerReloadBtn')?.addEventListener('click',loadPollerSettings);\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)=>`<div class=\"planner-hour-row\" data-hour=\"${hour}\"><span>${plannerHourLabel(hour)}</span><input id=\"plannerHour${hour}Down\" class=\"form-control form-control-sm planner-hour-input\" type=\"number\" min=\"0\" step=\"1024\" placeholder=\"DL B/s\"><input id=\"plannerHour${hour}Up\" class=\"form-control form-control-sm planner-hour-input\" type=\"number\" min=\"0\" step=\"1024\" placeholder=\"UL B/s\"><small id=\"plannerHour${hour}Summary\">Unlimited</small></div>`).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 `<span class=\"planner-diagnostic-item\"><b>${esc(label)}:</b> ${esc(value)}</span>`;\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=`<div><b><i class=\"fa-solid fa-sliders\"></i> Current settings</b><small class=\"planner-diagnostic-line\">${items.join('<i class=\"fa-solid fa-circle fa-2xs diagnostic-separator\" aria-hidden=\"true\"></i>')}</small></div>`;\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 <b>${esc(preview.matched_rule||'-')}</b> \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 : '<div class=\"empty-mini\">No Planner actions yet.</div>';\n const canToggle=Number(total||0)>10;\n const toggle=canToggle?`<button id=\"plannerHistoryToggle\" class=\"btn btn-xs btn-outline-secondary mt-2\"><i class=\"fa-solid fa-list\"></i> ${plannerHistoryExpanded?'Show last 10':'Show more'} (${esc(total)})</button>`:'';\n const clear=Number(total||0)?`<button id=\"plannerHistoryClear\" class=\"btn btn-xs btn-outline-danger mt-2 ms-2\"><i class=\"fa-solid fa-trash-can\"></i> Clear history</button>`:'';\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";
|