splij all js

This commit is contained in:
Mateusz Gruszczyński
2026-05-31 13:30:32 +02:00
parent 973d8d4774
commit 1068aba11c
62 changed files with 183 additions and 22 deletions
+122 -22
View File
@@ -1,13 +1,33 @@
import { stateSource } from './state.js';
import { torrentsSource } from './torrents.js';
import { stateCoreSource } from './stateCore.js';
import { columnStateSource } from './columnState.js';
import { runtimeStateSource } from './runtimeState.js';
import { sharedUiSource } from './sharedUi.js';
import { torrentFilterHelpersSource } from './torrentFilterHelpers.js';
import { torrentFilterUiSource } from './torrentFilterUi.js';
import { torrentTrackerFiltersSource } from './torrentTrackerFilters.js';
import { torrentTableStateSource } from './torrentTableState.js';
import { torrentActionStateSource } from './torrentActionState.js';
import { torrentRowRendererSource } from './torrentRowRenderer.js';
import { torrentTableRendererSource } from './torrentTableRenderer.js';
import { mobileSource } from './mobile.js';
import { messagesSource } from './messages.js';
import { torrentAddSource } from './torrentAdd.js';
import { apiSource } from './api.js';
import { createTorrentSource } from './createTorrent.js';
import { torrentDetailsSource } from './torrentDetails.js';
import { modalsSource } from './modals.js';
import { rssSource } from './rss.js';
import { torrentGeneralDetailsSource } from './torrentGeneralDetails.js';
import { torrentFileDetailsSource } from './torrentFileDetails.js';
import { torrentChunkDetailsSource } from './torrentChunkDetails.js';
import { torrentPeerDetailsSource } from './torrentPeerDetails.js';
import { torrentTrackerDetailsSource } from './torrentTrackerDetails.js';
import { mobileTorrentDetailsSource } from './mobileTorrentDetails.js';
import { torrentDetailsLoaderSource } from './torrentDetailsLoader.js';
import { pathPickerToolsSource } from './pathPickerTools.js';
import { columnManagerSource } from './columnManager.js';
import { jobToolsSource } from './jobTools.js';
import { labelToolsSource } from './labelTools.js';
import { ratioToolsSource } from './ratioTools.js';
import { rssToolsSource } from './rssTools.js';
import { backupToolsSource } from './backupTools.js';
import { smartQueueSource } from './smartQueue.js';
import { rtorrentConfigSource } from './rtorrentConfig.js';
import { appearancePreferencesSource } from './appearancePreferences.js';
@@ -25,25 +45,75 @@ import { appStatusSource } from './appStatus.js';
import { torrentStatsSource } from './torrentStats.js';
import { toolUiHelpersSource } from './toolUiHelpers.js';
import { authUsersSource } from './authUsers.js';
import { plannerSource } from './planner.js';
import { pollerSource } from './poller.js';
import { profilesSource } from './profiles.js';
import { dashboardSource } from './dashboard.js';
import { chartsSource } from './charts.js';
import { plannerToolsUiSource } from './plannerToolsUi.js';
import { plannerSpeedControlsSource } from './plannerSpeedControls.js';
import { plannerSettingsSource } from './plannerSettings.js';
import { plannerPreviewHistorySource } from './plannerPreviewHistory.js';
import { plannerActionsSource } from './plannerActions.js';
import { smartViewsSource } from './smartViews.js';
import { notificationCenterSource } from './notificationCenter.js';
import { diagnosticsDashboardSource } from './diagnosticsDashboard.js';
import { dashboardToolsSource } from './dashboardTools.js';
import { operationLogsSource } from './operationLogs.js';
import { bootstrapSource } from './bootstrap.js';
import { pollerSettingsSource } from './pollerSettings.js';
import { toolsModalSource } from './toolsModal.js';
import { toolPaneEventsSource } from './toolPaneEvents.js';
import { rssEventsSource } from './rssEvents.js';
import { smartQueueEventsSource } from './smartQueueEvents.js';
import { backupCleanupRtconfigEventsSource } from './backupCleanupRtconfigEvents.js';
import { automationEventsSource } from './automationEvents.js';
import { labelSmartEventsSource } from './labelSmartEvents.js';
import { torrentSelectionEventsSource } from './torrentSelectionEvents.js';
import { torrentTableEventsSource } from './torrentTableEvents.js';
import { preferenceEventsSource } from './preferenceEvents.js';
import { keyboardEventsSource } from './keyboardEvents.js';
import { speedLimitControlsSource } from './speedLimitControls.js';
import { themeMobileControlsSource } from './themeMobileControls.js';
import { jobSettingsSource } from './jobSettings.js';
import { profileListSource } from './profileList.js';
import { profileFormSource } from './profileForm.js';
import { profileActionsSource } from './profileActions.js';
import { profileSelectionSource } from './profileSelection.js';
import { realtimeChartsSource } from './realtimeCharts.js';
import { trafficHistoryDataSource } from './trafficHistoryData.js';
import { trafficChartRendererSource } from './trafficChartRenderer.js';
import { initialSnapshotSource } from './initialSnapshot.js';
import { footerStatusRefreshSource } from './footerStatusRefresh.js';
import { systemStatsSocketSource } from './systemStatsSocket.js';
import { mobileSelectEventsSource } from './mobileSelectEvents.js';
import { bootstrapRuntimeSource } from './bootstrapRuntime.js';
export const moduleSources = [
stateSource,
torrentsSource,
stateCoreSource,
columnStateSource,
runtimeStateSource,
sharedUiSource,
torrentFilterHelpersSource,
torrentFilterUiSource,
torrentTrackerFiltersSource,
torrentTableStateSource,
torrentActionStateSource,
torrentRowRendererSource,
torrentTableRendererSource,
mobileSource,
messagesSource,
torrentAddSource,
apiSource,
createTorrentSource,
torrentDetailsSource,
modalsSource,
rssSource,
torrentGeneralDetailsSource,
torrentFileDetailsSource,
torrentChunkDetailsSource,
torrentPeerDetailsSource,
torrentTrackerDetailsSource,
mobileTorrentDetailsSource,
torrentDetailsLoaderSource,
pathPickerToolsSource,
columnManagerSource,
jobToolsSource,
labelToolsSource,
ratioToolsSource,
rssToolsSource,
backupToolsSource,
smartQueueSource,
rtorrentConfigSource,
appearancePreferencesSource,
@@ -61,13 +131,43 @@ export const moduleSources = [
torrentStatsSource,
toolUiHelpersSource,
authUsersSource,
plannerSource,
dashboardSource,
plannerToolsUiSource,
plannerSpeedControlsSource,
plannerSettingsSource,
plannerPreviewHistorySource,
plannerActionsSource,
smartViewsSource,
notificationCenterSource,
diagnosticsDashboardSource,
dashboardToolsSource,
operationLogsSource,
pollerSource,
profilesSource,
chartsSource,
bootstrapSource,
pollerSettingsSource,
toolsModalSource,
toolPaneEventsSource,
rssEventsSource,
smartQueueEventsSource,
backupCleanupRtconfigEventsSource,
automationEventsSource,
labelSmartEventsSource,
torrentSelectionEventsSource,
torrentTableEventsSource,
preferenceEventsSource,
keyboardEventsSource,
speedLimitControlsSource,
themeMobileControlsSource,
jobSettingsSource,
profileListSource,
profileFormSource,
profileActionsSource,
profileSelectionSource,
realtimeChartsSource,
trafficHistoryDataSource,
trafficChartRendererSource,
initialSnapshotSource,
footerStatusRefreshSource,
systemStatsSocketSource,
mobileSelectEventsSource,
bootstrapRuntimeSource,
];
export function buildRuntimeSource(){
+1
View File
@@ -0,0 +1 @@
export const automationEventsSource = "$('statusPlannerOpen')?.addEventListener('click',()=>{ ensurePlannerToolsUI(); activateToolTab('planner'); new bootstrap.Modal($('toolsModal')).show(); }); $('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');});\n $('autoConditionType')?.addEventListener('change',updateAutomationForm); $('autoEffectType')?.addEventListener('change',updateAutomationForm); $('automationAddConditionBtn')?.addEventListener('click',()=>{automationConditions.push(automationCondition()); renderAutomationBuilder();}); $('automationAddEffectBtn')?.addEventListener('click',()=>{automationEffects.push(automationEffect()); renderAutomationBuilder();}); $('automationConditionList')?.addEventListener('click',e=>{const b=e.target.closest('.automation-remove-condition'); if(!b)return; automationConditions.splice(Number(b.dataset.index||0),1); renderAutomationBuilder();}); $('automationEffectList')?.addEventListener('click',e=>{const b=e.target.closest('.automation-remove-effect'); if(!b)return; automationEffects.splice(Number(b.dataset.index||0),1); renderAutomationBuilder();}); $('automationCancelEditBtn')?.addEventListener('click',resetAutomationForm); $('automationSaveBtn')?.addEventListener('click',saveAutomation); $('automationExportBtn')?.addEventListener('click',exportAutomations); $('automationImportBtn')?.addEventListener('click',()=>$('automationImportFile')?.click()); $('automationImportFile')?.addEventListener('change',e=>importAutomations(e.target.files?.[0])); $('automationCheckBtn')?.addEventListener('click',async()=>{setBusy(true);try{const j=await post('/api/automations/check',{}); const torrents=j.result?.applied?.length||0; const batches=j.result?.batches?.length||0; toastMessage('toast.automationsApplied','success',{count:torrents,batches}); await loadAutomations();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('automationManager')?.addEventListener('click',async e=>{const run=e.target.closest('.automation-run'); if(run){ setBusy(true); try{ const j=await post(`/api/automations/${run.dataset.id}/run`,{}); toastMessage('toast.automationForceRunDone','success',{count:j.result?.applied?.length}); await loadAutomations(); }catch(err){ toast(err.message,'danger'); } finally{ setBusy(false); } return; } const toggle=e.target.closest('.automation-toggle'); if(toggle){ await toggleAutomationRule(automationRulesCache.find(r=>String(r.id)===String(toggle.dataset.id))); return; } const edit=e.target.closest('.automation-edit'); if(edit){ editAutomationRule(automationRulesCache.find(r=>String(r.id)===String(edit.dataset.id))); return; } 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();}); $('automationHistory')?.addEventListener('click',e=>{ if(e.target.closest('#automationClearHistoryBtn')) clearAutomationHistory(); });\n ";
@@ -0,0 +1 @@
export const backupCleanupRtconfigEventsSource = "$('profileBackupCreateBtn')?.addEventListener('click',async()=>{await post('/api/backup/profile',{name:$('profileBackupName')?.value||'Profile backup'}); toast('Profile backup created','success'); loadBackup();}); $('appBackupCreateBtn')?.addEventListener('click',async()=>{await post('/api/backup/app',{name:$('appBackupName')?.value||'Application backup'}); toast('Application backup created','success'); loadBackup();}); $('profileBackupSettingsSaveBtn')?.addEventListener('click',async()=>{await post('/api/backup/profile/settings',{enabled:$('profileBackupAutoEnabled')?.checked,interval_hours:Number($('profileBackupAutoInterval')?.value||24),retention_days:Number($('profileBackupRetentionDays')?.value||30)}); toast('Profile backup schedule saved','success'); loadBackup();}); $('backupSettingsSaveBtn')?.addEventListener('click',async()=>{await post('/api/backup/settings',{enabled:$('backupAutoEnabled')?.checked,interval_hours:Number($('backupAutoInterval')?.value||24),retention_days:Number($('backupRetentionDays')?.value||30)}); toast('Application backup schedule saved','success'); loadBackup();}); document.querySelectorAll('[data-backup-pane]').forEach(tab=>tab.addEventListener('click',()=>{ if(tab.classList.contains('disabled')) return; switchBackupPane(tab.dataset.backupPane||'profile'); })); const backupClickHandler=async e=>{const preview=e.target.closest('.backup-preview-btn'); const restore=e.target.closest('.backup-restore'); const del=e.target.closest('.backup-delete'); if(preview){ const j=await (await fetch(`/api/backup/${preview.dataset.id}/preview`)).json(); if(!j.ok) throw new Error(j.error||'Backup preview failed'); const box=$('backupPreview'); if(box){ box.classList.remove('d-none'); box.innerHTML=backupPreviewTable(j.preview||{}); box.scrollIntoView({block:'nearest'}); } return; } if(restore){ const type=restore.dataset.type==='app'?'application':'profile'; const msg=type==='application'?'Restore this application backup and replace users, profiles and global settings?':'Restore this profile backup into the current active profile?'; if(!confirm(msg)) return; await post(`/api/backup/${restore.dataset.id}/restore`,{}); toast('Backup restored','success'); loadBackup(); return; } if(del){ if(!confirm('Delete this backup permanently?')) return; await post(`/api/backup/${del.dataset.id}`,{},'DELETE'); toast('Backup deleted','success'); loadBackup(); }}; $('profileBackupManager')?.addEventListener('click',backupClickHandler); $('appBackupManager')?.addEventListener('click',backupClickHandler); $('cleanupManager')?.addEventListener('click',async e=>{ if(e.target.closest('#cleanupRefreshBtn')) return loadCleanup(); if(e.target.closest('#cleanupProfileCacheBtn')) return runCleanupAction('/api/cleanup/cache','Clear active profile cache'); if(e.target.closest('#cleanupPollerDiagnosticsBtn')) return runCleanupAction('/api/cleanup/poller-diagnostics','Reset poller diagnostics'); 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('#cleanupOperationLogsBtn')) return runCleanupAction('/api/cleanup/operation-logs','Clear operation logs'); if(e.target.closest('#cleanupPlannerBtn')) return runCleanupAction('/api/cleanup/planner','Clear Planner logs'); if(e.target.closest('#cleanupAutomationsBtn')) return runCleanupAction('/api/cleanup/automations','Clear automation logs'); if(e.target.closest('#cleanupAllBtn')) return runCleanupAction('/api/cleanup/all','Clear job, Smart Queue, operation, Planner and automation logs'); }); $('rtConfigReloadBtn')?.addEventListener('click',loadRtConfig); $('rtConfigResetBtn')?.addEventListener('click',resetRtConfig); $('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); ";
+1
View File
@@ -0,0 +1 @@
export const backupToolsSource = " function fillBackupSettings(settings={}, prefix='app'){\n const cap=prefix==='profile'?'Profile':'App';\n const enabled=$(prefix==='profile'?'profileBackupAutoEnabled':'backupAutoEnabled');\n const interval=$(prefix==='profile'?'profileBackupAutoInterval':'backupAutoInterval');\n const retention=$(prefix==='profile'?'profileBackupRetentionDays':'backupRetentionDays');\n if(enabled) enabled.checked=!!settings.enabled;\n if(interval) interval.value=settings.interval_hours||24;\n if(retention) retention.value=settings.retention_days||30;\n }\n function backupPreviewDetails(table={}){\n const sample=table.sample||[];\n if(!sample.length) return '<div class=\"backup-preview-empty\">No saved rows in this table.</div>';\n const keys=[...new Set(sample.flatMap(row=>Object.keys(row||{})))].slice(0,8);\n return responsiveTable(keys.map(esc), sample.map(row=>keys.map(key=>esc(row?.[key] ?? ''))), 'backup-preview-sample-table');\n }\n function backupPreviewTable(preview={}){\n const tables=preview.tables||[];\n const rows=tables.map(t=>`<details class=\"backup-preview-table-details\"><summary><span><b>${esc(t.name)}</b><small>${esc(t.rows)} row(s) · ${(t.columns||[]).length} column(s)</small></span></summary>${backupPreviewDetails(t)}</details>`).join('');\n const type=preview.backup_type==='app'?'application':'profile';\n return `<div class=\"surface-section backup-preview-card\"><div class=\"section-title\"><i class=\"fa-solid fa-eye\"></i> Backup preview</div><div class=\"small text-muted mb-2\">${esc(type)} backup · Created: ${esc(preview.created_at||'-')} · ${preview.automatic?'automatic':'manual'} · sensitive values hidden</div>${rows || '<div class=\"empty-mini\">Backup has no previewable settings.</div>'}</div>`;\n }\n function backupRows(rows=[]){\n return responsiveTable(['Name','Created','Type','Actions'],rows.map(b=>[esc(b.name),humanDateCell(b.created_at),b.automatic?'Auto':'Manual',`<div class=\"table-action-group backup-actions\"><button class=\"btn btn-xs btn-outline-info backup-preview-btn\" data-id=\"${esc(b.id)}\"><i class=\"fa-solid fa-eye\"></i> Preview</button><a class=\"btn btn-xs btn-outline-secondary\" href=\"/api/backup/${esc(b.id)}/download\"><i class=\"fa-solid fa-download\"></i> Download</a><button class=\"btn btn-xs btn-outline-warning backup-restore\" data-id=\"${esc(b.id)}\" data-type=\"${esc(b.backup_type||'profile')}\"><i class=\"fa-solid fa-rotate-left\"></i> Restore</button><button class=\"btn btn-xs btn-outline-danger backup-delete\" data-id=\"${esc(b.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button></div>`]),'backup-table');\n }\n function switchBackupPane(pane){\n document.querySelectorAll('[data-backup-pane]').forEach(x=>x.classList.toggle('active',x.dataset.backupPane===pane));\n document.querySelectorAll('[data-backup-panel]').forEach(x=>x.classList.toggle('d-none',x.dataset.backupPanel!==pane));\n }\n async function loadBackup(){\n const j=await (await fetch('/api/backup')).json();\n fillBackupSettings(j.profile_auto||{}, 'profile');\n fillBackupSettings(j.app_auto||j.auto||{}, 'app');\n if($('profileBackupManager')) $('profileBackupManager').innerHTML=backupRows(j.profile_backups||[]);\n if($('appBackupManager')) $('appBackupManager').innerHTML=j.can_app_backup ? backupRows(j.app_backups||[]) : '<div class=\"empty-mini\">Application backups are admin-only.</div>';\n if(!j.can_app_backup) document.querySelector('[data-backup-pane=\"app\"]')?.classList.add('disabled');\n }\n";
+1
View File
@@ -0,0 +1 @@
export const bootstrapRuntimeSource = " updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\n";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const columnStateSource = " const COLUMN_DEFS = [[\"status\",\"Status\",false],[\"size\",\"Size\",false],[\"progress\",\"Progressbar\",false],[\"down_rate\",\"DL\",false],[\"up_rate\",\"UL\",false],[\"eta\",\"ETA\",false],[\"seeds\",\"Seeds\",false],[\"peers\",\"Peers\",false],[\"ratio\",\"Ratio\",false],[\"path\",\"Path\",false],[\"label\",\"Label\",false],[\"ratio_group\",\"Ratio group\",false],[\"down_total\",\"Downloaded\",true],[\"to_download\",\"To download\",true],[\"up_total\",\"Uploaded\",true],[\"created\",\"Created\",true],[\"last_activity\",\"Last activity\",true],[\"priority\",\"Priority\",true],[\"state\",\"State\",true],[\"active\",\"Active\",true],[\"complete\",\"Complete\",true],[\"hashing\",\"Hashing\",true],[\"message\",\"Message\",true],[\"hash\",\"Hash\",true]];\n const DEFAULT_HIDDEN_COLUMNS = new Set(COLUMN_DEFS.filter(([, , hiddenByDefault]) => hiddenByDefault).map(([key]) => key));\n const savedColumns = window.PYTORRENT?.tableColumns || {};\n const DEFAULT_COLUMN_WIDTHS = {\n select: 34, name: 360, status: 110, size: 90, progress: 120,\n down_rate: 86, up_rate: 86, eta: 92, seeds: 70, peers: 70,\n ratio: 72, path: 300, label: 140, ratio_group: 130,\n down_total: 120, to_download: 120, up_total: 120, created: 150,\n last_activity: 150, priority: 80, state: 70, active: 70, complete: 82, hashing: 82,\n message: 220, hash: 280\n };\n const COLUMN_WIDTH_MIN = 44;\n const COLUMN_WIDTH_MAX = 720;\n const explicitlyShownColumns = new Set(savedColumns.shown || []);\n let hiddenColumns = new Set([...(savedColumns.hidden || []), ...[...DEFAULT_HIDDEN_COLUMNS].filter(key => !explicitlyShownColumns.has(key))]);\n // Note: Column widths are persisted with the existing column preferences payload, so no database migration is needed.\n function normalizeColumnWidths(value={}){\n const allowed = new Set(['select', ...COLUMN_DEFS.map(([key]) => key)]);\n const normalized = {...DEFAULT_COLUMN_WIDTHS};\n Object.entries(value || {}).forEach(([key, width])=>{\n if(allowed.has(key)) normalized[key] = clampNumber(width, COLUMN_WIDTH_MIN, COLUMN_WIDTH_MAX, DEFAULT_COLUMN_WIDTHS[key] || 120);\n });\n return normalized;\n }\n let columnWidths = normalizeColumnWidths(savedColumns.widths || {});\n if(browserViewPrefs.columnWidths) columnWidths = normalizeColumnWidths({...columnWidths, ...browserViewPrefs.columnWidths});\n function mobileSortStepId(step){ return `${step.key}:${step.dir}`; }\n function normalizeMobileSortFilters(value={}){\n const normalized = Object.fromEntries(MOBILE_SORT_STEPS.map(step => {\n const id = mobileSortStepId(step);\n return [id, DEFAULT_MOBILE_SORT_FILTER_IDS.has(id)];\n }));\n Object.entries(value || {}).forEach(([id, enabled]) => { if(id in normalized) normalized[id] = !!enabled; });\n return normalized;\n }\n let mobileSortFilters = normalizeMobileSortFilters(savedColumns.mobileSortFilters || {});\n if(browserViewPrefs.mobileSortFilters) mobileSortFilters = normalizeMobileSortFilters({...mobileSortFilters, ...browserViewPrefs.mobileSortFilters});\n const DEFAULT_MOBILE_COLUMNS = new Set([\"status\",\"progress\",\"down_rate\",\"up_rate\",\"eta\",\"seeds\",\"peers\",\"ratio\",\"path\"]);\n const MOBILE_COLUMN_DEFS = COLUMN_DEFS.map(([key,label]) => [key, label, DEFAULT_MOBILE_COLUMNS.has(key)]);\n function normalizeMobileColumns(value={}){\n const normalized = {...Object.fromEntries(MOBILE_COLUMN_DEFS.map(([key,,shown])=>[key, shown]))};\n Object.entries(value || {}).forEach(([key, shown])=>{\n if(key === \"speed\"){ normalized.down_rate = !!shown; normalized.up_rate = !!shown; }\n else if(key === \"seed_peer\"){ normalized.seeds = !!shown; normalized.peers = !!shown; }\n else if(key in normalized) normalized[key] = !!shown;\n });\n return normalized;\n }\n let mobileColumns = normalizeMobileColumns(savedColumns.mobile || {});\n if(browserViewPrefs.mobileColumns) mobileColumns = normalizeMobileColumns({...mobileColumns, ...browserViewPrefs.mobileColumns});\n let mobileSmartFiltersEnabled = browserViewPrefs.mobileSmartFiltersEnabled ?? savedColumns.mobileSmartFiltersEnabled ?? true;\n";
+1
View File
@@ -0,0 +1 @@
export const dashboardToolsSource = "function ensureDashboardToolsUI(){\n const host=$('toolRss')?.parentElement || document.querySelector('#toolsModal .modal-body');\n if(!host) return;\n addToolTab('smartviews','fa-layer-group','Smart Views','torrentstats');\n addToolTab('notifications','fa-bell','Notifications','appstatus');\n const stats=$('toolTorrentStats');\n if(stats && !$('healthDashboardManager')){\n const section=document.createElement('div');\n section.className='surface-section mt-3';\n section.innerHTML='<div class=\"section-title\"><i class=\"fa-solid fa-heart-pulse\"></i> Torrent health</div><div class=\"tool-note mb-3\">Live health buckets calculated from the current torrent snapshot.</div><div id=\"healthDashboardManager\"></div>';\n stats.appendChild(section);\n section.addEventListener('click',e=>{ const tab=e.target.closest('[data-health-pane]'); if(tab){ const pane=tab.dataset.healthPane; section.querySelectorAll('[data-health-pane]').forEach(x=>x.classList.toggle('active',x.dataset.healthPane===pane)); section.querySelectorAll('[data-health-panel]').forEach(x=>x.classList.toggle('d-none',x.dataset.healthPanel!==pane)); return; } const row=e.target.closest('[data-hash]'); if(!row) return; selectedHash=row.dataset.hash; selected.clear(); selected.add(selectedHash); scheduleRender(true); });\n }\n if(!$('toolSmartviews')){\n const p=document.createElement('div');\n p.id='toolSmartviews';\n p.className='d-none';\n p.innerHTML='<div class=\"surface-section\"><div class=\"section-title\"><i class=\"fa-solid fa-layer-group\"></i> Smart Views</div><div class=\"tool-note mb-3\">One-click filters for common torrent maintenance tasks.</div><div id=\"smartViewsManager\"></div></div>';\n host.appendChild(p);\n p.addEventListener('click',e=>{ const card=e.target.closest('.smart-view-card'); if(!card) return; activeTrackerFilter=''; activeFilter=card.dataset.filter||'all'; mobileActiveFilterKey=activeFilter; saveActiveFilterPreference(); syncFilterButtons(); scheduleRender(true); renderSmartViewsManager(); });\n }\n if(!$('toolNotifications')){\n const p=document.createElement('div');\n p.id='toolNotifications';\n p.className='d-none';\n p.innerHTML='<div class=\"surface-section\"><div class=\"section-title\"><i class=\"fa-solid fa-bell\"></i> Notification center</div><div class=\"tool-note mb-3\">Persistent local history for rTorrent, RSS, automation, disk, queue, planner and port events.</div><div id=\"notificationCenterManager\"></div></div>';\n host.appendChild(p);\n }\n renderHealthDashboard();\n renderSmartViewsManager();\n renderNotificationCenter();\n updateNotificationBadge();\n}\n";
@@ -0,0 +1 @@
export const diagnosticsDashboardSource = "function diagnosticsSection(title, cards){\n return `<section class=\"diagnostics-section\"><div class=\"section-title\"><i class=\"fa-solid fa-stethoscope\"></i> ${esc(title)}</div><div class=\"diag-grid\">${cards.join('')}</div></section>`;\n}\nasync function loadDiagnosticsPage(){\n const box=$('diagnosticsPageManager');\n if(!box) return;\n box.innerHTML='<span class=\"spinner-border spinner-border-sm\"></span> Loading diagnostics...';\n try{\n const [status,poller,planner,smart]=await Promise.all([\n fetch('/api/app/status').then(r=>r.json()),\n fetch('/api/poller/settings').then(r=>r.json()).catch(()=>({})),\n fetch('/api/download-planner/preview').then(r=>r.json()).catch(()=>({})),\n fetch('/api/smart-queue?history_limit=100').then(r=>r.json()).catch(()=>({ok:false})),\n ]);\n if(status && status.ok===false) throw new Error(status.error||'Failed to load diagnostics');\n const st=status.status||{}, profile=st.profile||{}, py=st.pytorrent||{}, scgi=st.scgi||{}, cleanup=st.cleanup||{}, db=cleanup.database||{}, pc=st.port_check||{};\n const rt=poller.runtime||{}, ps=poller.settings||{}, pv=planner.preview||{}, smartStats=smart?.ok?buildSmartQueueNerdStats(smart.history||[], Number(smart.history_total||0)):null;\n const profileCards=[diagCard('Active profile', profile.name||profile.id||'-'), diagCard('API response time', `${st.api_ms ?? '-'} ms`), diagCard('Incoming port', pc.port||'-'), diagCard('Port status', portStatusLabel(pc.status), pc.status==='closed'?'diag-error':'')];\n const rtCards=[diagCard('SCGI status', scgi.ok?'OK':'ERROR', scgi.ok?'':'diag-error'), diagCard('SCGI URL', scgi.url||'-'), diagCard('Connect', scgi.connect_ms!=null?`${scgi.connect_ms} ms`:'-'), diagCard('First byte', scgi.first_byte_ms!=null?`${scgi.first_byte_ms} ms`:'-'), diagCard('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||'-')];\n const pollerCards=[diagCard('Adaptive', ps.adaptive_enabled===false?'off':'on'), diagCard('Mode', rt.adaptive_mode||'-'), diagCard('Effective interval', `${rt.effective_interval_seconds??'-'}s`), diagCard('Minimum interval', `${rt.configured_min_interval_seconds??'-'}s`), diagCard('Tick duration', `${rt.duration_ms||rt.last_tick_ms||0} ms`), diagCard('Tick gap', `${rt.last_tick_gap_ms||0} ms`), diagCard('Payload', fmtBytes(rt.emitted_payload_size||0)), diagCard('rTorrent calls', rt.rtorrent_call_count||0), diagCard('Skipped emissions', rt.skipped_emissions||0), diagCard('Ticks', rt.tick_count||0)];\n const plannerCards=[diagCard('Matched rule', pv.matched_rule||'-'), diagCard('Next change', pv.next_change_at||'-'), diagCard('Planner state', pv.enabled===false?'disabled':'enabled')];\n const databaseCards=[diagCard('DB size', db.size_h||'-'), diagCard('Job logs', cleanup.jobs_clearable ?? '-'), diagCard('Smart Queue logs', cleanup.smart_queue_history_total ?? '-'), diagCard('Automation logs', cleanup.automation_history_total ?? '-')];\n const workerCards=[diagCard('Worker threads', py.worker_threads ?? '-'), diagCard('Jobs total', py.jobs_total ?? '-'), diagCard('Threads', py.threads ?? '-'), diagCard('CPU', `${py.cpu_percent ?? '-'}%`)];\n const smartBlock=`<section class=\"diagnostics-section\"><div class=\"section-title\"><i class=\"fa-solid fa-list-check\"></i> Smart Queue decisions</div>${renderSmartQueueNerdStats(smartStats)}</section>`;\n box.innerHTML=[diagnosticsSection('Profile and port',profileCards), diagnosticsSection('rTorrent connection',rtCards), diagnosticsSection('Adaptive poller',pollerCards), diagnosticsSection('Planner',plannerCards), diagnosticsSection('Database and cleanup',databaseCards), diagnosticsSection('Worker state',workerCards), smartBlock, scgi.error?`<div class=\"alert alert-danger mt-3 mb-0\">${esc(scgi.error)}</div>`:''].join('');\n }catch(e){ box.innerHTML=`<div class=\"alert alert-danger mb-0\">${esc(e.message)}</div>`; }\n}\n";
@@ -0,0 +1 @@
export const footerStatusRefreshSource = " function rtorrentPairText(current, max){\n if(current == null) return '-';\n return max == null ? String(current) : `${current}/${max}`;\n }\n function footerStatusUpdatedText(s={}){\n const value=s.footer_updated_at || s.updated_at;\n if(!value) return '';\n const date=new Date(value);\n return Number.isNaN(date.getTime()) ? '' : ` · last known ${date.toLocaleString()}`;\n }\n function updateRtorrentFooterStats(s={}, cached=false){\n const suffix=cached ? footerStatusUpdatedText(s) : '';\n const sockets=rtorrentPairText(s.open_sockets, s.max_open_sockets);\n if($('statSockets')) $('statSockets').textContent=sockets;\n if($('statusSockets')) $('statusSockets').title=s.open_sockets == null ? `Open sockets unavailable${suffix}` : `Open rTorrent sockets${s.max_open_sockets == null ? '' : ' / max'}: ${sockets}${suffix}`;\n if($('statRtDownloads')) $('statRtDownloads').textContent=rtorrentPairText(s.active_downloads, s.max_downloads_global);\n if($('statusRtDownloads')) $('statusRtDownloads').title=`Active rTorrent downloads / max global downloads${suffix}`;\n if($('statRtUploads')) $('statRtUploads').textContent=rtorrentPairText(s.active_uploads, s.max_uploads_global);\n if($('statusRtUploads')) $('statusRtUploads').title=`Active rTorrent uploads / max global uploads${suffix}`;\n if($('statRtHttp')) $('statRtHttp').textContent=rtorrentPairText(s.open_http, s.max_open_http);\n if($('statusRtHttp')) $('statusRtHttp').title=`Open rTorrent HTTP connections / max HTTP connections${suffix}`;\n if($('statRtFiles')) $('statRtFiles').textContent=rtorrentPairText(s.open_files, s.max_open_files);\n if($('statusRtFiles')) $('statusRtFiles').title=`Open rTorrent files / max open files${suffix}`;\n if($('statRtPort')) $('statRtPort').textContent=(s.listen_port ?? '-') || '-';\n if($('statusRtPort')) $('statusRtPort').title=`rTorrent incoming port${suffix}`;\n if(cached){\n if(s.cpu!==undefined && $('statCpu')) $('statCpu').textContent=s.cpu;\n if(s.ram!==undefined && $('statRam')) $('statRam').textContent=s.ram;\n if(s.version!==undefined && $('statVersion')) $('statVersion').textContent=s.version || '-';\n if(s.down_rate_h!==undefined && $('statDl')) $('statDl').textContent=s.down_rate_h || '0 B/s';\n if(s.up_rate_h!==undefined && $('statUl')) $('statUl').textContent=s.up_rate_h || '0 B/s';\n if(s.down_rate_h!==undefined && $('mobileSpeedDl')) $('mobileSpeedDl').textContent=s.down_rate_h || '0 B/s';\n if(s.up_rate_h!==undefined && $('mobileSpeedUl')) $('mobileSpeedUl').textContent=s.up_rate_h || '0 B/s';\n updateBrowserSpeedTitle(s.down_rate_h, s.up_rate_h);\n }\n }\n function saveFooterStatusCache(s={}){\n const payload={\n open_sockets:s.open_sockets, max_open_sockets:s.max_open_sockets,\n active_downloads:s.active_downloads, max_downloads_global:s.max_downloads_global,\n active_uploads:s.active_uploads, max_uploads_global:s.max_uploads_global,\n open_http:s.open_http, max_open_http:s.max_open_http,\n open_files:s.open_files, max_open_files:s.max_open_files,\n listen_port:s.listen_port,\n cpu:s.cpu, ram:s.ram, version:s.version,\n down_rate_h:s.down_rate_h, up_rate_h:s.up_rate_h,\n footer_updated_at:new Date().toISOString()\n };\n try{ localStorage.setItem(FOOTER_STATUS_STORAGE_KEY, JSON.stringify(payload)); }catch(_){}\n }\n function restoreFooterStatusCache(){\n try{\n const cached=JSON.parse(localStorage.getItem(FOOTER_STATUS_STORAGE_KEY)||'null');\n if(cached && typeof cached==='object') updateRtorrentFooterStats(cached, true);\n }catch(_){}\n }\n async function refreshFooterStatusNow(){\n try{\n const res=await fetch('/api/system/status', {cache:'no-store'});\n const j=await res.json();\n const s=j.status||{};\n if(j.ok && s){\n updateRtorrentFooterStats(s, false);\n saveFooterStatusCache(s);\n applyFooterPreferences();\n }\n }catch(_){}\n }\n";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const jobSettingsSource = " async function activeProfileForSettings(){\n const j=await (await fetch('/api/profiles')).json();\n return j.active || (j.profiles||[])[0] || null;\n }\n function fillJobSettings(profile){\n if(!profile) return;\n if($('jobHeavyParallel')) $('jobHeavyParallel').value=profile.max_parallel_jobs||5;\n if($('jobLightParallel')) $('jobLightParallel').value=profile.light_parallel_jobs||4;\n if($('jobLightTimeout')) $('jobLightTimeout').value=profile.light_job_timeout_seconds||300;\n if($('jobHeavyTimeout')) $('jobHeavyTimeout').value=profile.heavy_job_timeout_seconds||7200;\n if($('jobPendingTimeout')) $('jobPendingTimeout').value=profile.pending_job_timeout_seconds||900;\n if($('jobSettingsProfileName')) $('jobSettingsProfileName').textContent=profile.name?`Active profile: ${profile.name}`:'';\n }\n async function loadJobSettings(){\n try{\n const profile=await activeProfileForSettings();\n if(!profile){ if($('jobSettingsProfileName')) $('jobSettingsProfileName').textContent='No active profile.'; return; }\n fillJobSettings(profile);\n }catch(e){ if($('jobSettingsProfileName')) $('jobSettingsProfileName').textContent=e.message; }\n }\n function jobSettingsPayload(profile){\n return {\n name:profile.name,\n scgi_url:profile.scgi_url,\n timeout_seconds:profile.timeout_seconds||5,\n max_parallel_jobs:$('jobHeavyParallel')?.value||5,\n light_parallel_jobs:$('jobLightParallel')?.value||4,\n light_job_timeout_seconds:$('jobLightTimeout')?.value||300,\n heavy_job_timeout_seconds:$('jobHeavyTimeout')?.value||7200,\n pending_job_timeout_seconds:$('jobPendingTimeout')?.value||900,\n is_remote:!!profile.is_remote,\n is_default:!!profile.is_default\n };\n }\n async function saveJobSettings(){\n const btn=$('saveJobSettingsBtn');\n buttonBusy(btn,true);\n try{\n const profile=await activeProfileForSettings();\n if(!profile) throw new Error('No active profile');\n const j=await post(`/api/profiles/${profile.id}`,jobSettingsPayload(profile),'PUT');\n fillJobSettings(j.profile||profile);\n await refreshProfiles();\n toast('Job settings saved','success');\n }catch(e){ toast(e.message,'danger'); }\n finally{ buttonBusy(btn,false); }\n }\n";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const keyboardEventsSource = "document.addEventListener('keydown',e=>{ const tag=(e.target?.tagName||'').toLowerCase(); const editable=tag==='input'||tag==='textarea'||tag==='select'||e.target?.isContentEditable; if(editable){ if(e.key==='Enter' && e.target?.id==='labelInput'){ e.preventDefault(); $('addLabelToSelectionBtn')?.click(); } return; } if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='a'){e.preventDefault();selected.clear();visibleRows.forEach(t=>selected.add(t.hash));scheduleRender(true);} if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='i'){e.preventDefault();visibleRows.forEach(t=>selected.has(t.hash)?selected.delete(t.hash):selected.add(t.hash));scheduleRender(true);} if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='o'){e.preventDefault();new bootstrap.Modal($('addModal')).show();} if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){e.preventDefault();downloadTorrentFiles();return;} if(e.key==='Escape'){selected.clear();scheduleRender(true);} if(e.key==='Delete') new bootstrap.Modal($('removeModal')).show(); if(e.key===' ') {e.preventDefault();runAction('start');} if(e.key.toLowerCase()==='p')runAction('pause'); if(e.key.toLowerCase()==='s' && !(e.ctrlKey||e.metaKey))runAction('stop'); if(e.key.toLowerCase()==='r')runAction('resume'); if(e.key.toLowerCase()==='m')runAction('move'); });\n $('removeModal')?.addEventListener('show.bs.modal',()=>{$('removeCount').textContent=selected.size;$('removeData').checked=true;}); $('confirmRemoveBtn')?.addEventListener('click',async()=>{await runAction('remove',{remove_data:$('removeData').checked});bootstrap.Modal.getInstance($('removeModal'))?.hide();});\n $('addModal')?.addEventListener('show.bs.modal',()=>applyDefaultDownloadPath(true));\n\n $('toolsModal')?.addEventListener('show.bs.modal',()=>applyDefaultDownloadPath(false));\n // Note: Torrent add modal and drag/drop upload handling moved to torrentAdd.js.\n ";
+1
View File
@@ -0,0 +1 @@
export const labelSmartEventsSource = "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);} });\n $('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);});\n $('smartExcludeSelectedBtn')?.addEventListener('click',openSmartQueueExclusionModal);\n $('smartExclusionSearch')?.addEventListener('input',filterSmartQueueExclusionChoices);\n $('smartExclusionOnlySelected')?.addEventListener('change',filterSmartQueueExclusionChoices);\n $('smartExclusionSelectVisibleBtn')?.addEventListener('click',()=>setSmartQueueVisibleExceptions(true));\n $('smartExclusionClearVisibleBtn')?.addEventListener('click',()=>setSmartQueueVisibleExceptions(false));\n $('smartExclusionSaveBtn')?.addEventListener('click',saveSmartQueueExclusionChoices);\n $('smartHistory')?.addEventListener('click',async e=>{\n const clear=e.target.closest('#smartHistoryClear');\n if(clear){\n // Note: Clear history removes only Smart Queue audit rows for the active profile.\n if(!confirm('Clear Smart Queue history?')) return;\n try{ await post('/api/smart-queue/history',{},'DELETE'); smartHistoryExpanded=false; toast('Smart Queue history cleared','success'); await loadSmartQueue(); }catch(err){ toast(err.message,'danger'); }\n return;\n }\n const btn=e.target.closest('#smartHistoryToggle'); if(!btn) return; smartHistoryExpanded=!smartHistoryExpanded; loadSmartQueue();\n });\n\n // Note: Mobile filter changes are handled by setMobileFilterValue in bootstrap.js to avoid duplicate preference writes.\n ";
+1
View File
@@ -0,0 +1 @@
export const labelToolsSource = " async function loadLabels(){ const j=await (await fetch('/api/labels')).json(); const labels=j.labels||[]; knownLabels=labels; renderLabelFilters(); renderLabelChooser(); if($('labelsManager')) $('labelsManager').innerHTML=labels.length?labels.map(l=>`<div class=\"label-manager-row\"><span class=\"chip\"><i class=\"fa-solid fa-tag\"></i> ${esc(l.name)}</span><button class=\"btn btn-xs btn-outline-danger delete-label\" data-id=\"${esc(l.id)}\" title=\"Delete label\"><i class=\"fa-solid fa-trash-can\"></i> Remove</button></div>`).join(''):'<div class=\"empty-state\"><i class=\"fa-solid fa-tags\"></i><b>No labels.</b><span>Add first label above.</span></div>'; }\n function renderLabelChooser(){ if($('selectedLabelList')) $('selectedLabelList').innerHTML=[...modalLabels].map(l=>`<button class=\"chip label-selected\" data-label=\"${esc(l)}\" title=\"Remove\"><i class=\"fa-solid fa-tag\"></i> ${esc(l)} <i class=\"fa-solid fa-xmark ms-1\"></i></button>`).join('') || '<span class=\"text-muted small\">No labels selected.</span>'; if($('labelList')) $('labelList').innerHTML=knownLabels.map(l=>`<button class=\"chip label-chip ${modalLabels.has(l.name)?'active':''}\" data-label=\"${esc(l.name)}\"><i class=\"fa-solid fa-tag\"></i> ${esc(l.name)}</button>`).join('') || '<span class=\"text-muted small\">No saved labels.</span>'; }\n async function saveKnownLabel(name){ name=String(name||'').trim(); if(!name) return; await post('/api/labels',{name}); await loadLabels(); }\n";
@@ -0,0 +1 @@
export const mobileSelectEventsSource = " document.addEventListener('change',e=>{ const sort=e.target.closest('#mobileSortSelect'); if(sort){ setMobileSortValue(sort.value); return; } const sel=e.target.closest('#mobileFilterSelect'); if(!sel) return; setMobileFilterValue(sel.value); });\n ";
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const notificationCenterSource = "function notificationItems(){\n try{ return JSON.parse(localStorage.getItem(NOTIFICATION_STORAGE_KEY)||'[]'); }catch(e){ return []; }\n}\nfunction saveNotificationItems(items){ localStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(items.slice(0,120))); }\nfunction recordNotification(type, title, message){\n const item={at:new Date().toISOString(), type:String(type||'info'), title:String(title||type||'Notification'), message:String(message||'')};\n const items=[item,...notificationItems()].slice(0,120);\n saveNotificationItems(items);\n renderNotificationCenter();\n updateNotificationBadge();\n}\nfunction notificationIcon(type){\n if(type==='error') return 'fa-triangle-exclamation';\n if(type==='warning') return 'fa-circle-exclamation';\n if(type==='planner') return 'fa-calendar-days';\n if(type==='queue') return 'fa-shuffle';\n return 'fa-circle-info';\n}\nfunction updateNotificationBadge(){\n const btn=document.querySelector('.tool-tab[data-tool=\"notifications\"]');\n if(!btn) return;\n const count=notificationItems().length;\n btn.innerHTML=`<i class=\"fa-solid fa-bell\"></i> Notifications${count?` <span class=\"badge text-bg-danger\">${count}</span>`:''}`;\n}\nfunction renderNotificationCenter(){\n const box=$('notificationCenterManager');\n if(!box) return;\n const items=notificationItems();\n box.innerHTML=`<div class=\"notification-toolbar\"><button id=\"clearNotificationsBtn\" class=\"btn btn-sm btn-outline-danger\" type=\"button\"><i class=\"fa-solid fa-trash\"></i> Clear</button><span>${esc(items.length)} saved event(s)</span></div><div class=\"notification-list\">${items.map(x=>`<article class=\"notification-item notification-${esc(x.type)}\"><i class=\"fa-solid ${notificationIcon(x.type)}\"></i><div><b>${esc(x.title)}</b><span>${esc(x.message)}</span><small>${esc(new Date(x.at).toLocaleString())}</small></div></article>`).join('')||'<span class=\"empty-mini\">No notifications yet.</span>'}</div>`;\n $('clearNotificationsBtn')?.addEventListener('click',()=>{ saveNotificationItems([]); renderNotificationCenter(); updateNotificationBadge(); });\n}\n";
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const plannerPreviewHistorySource = "";
+1
View File
@@ -0,0 +1 @@
export const plannerSettingsSource = " 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?' · 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";
@@ -0,0 +1 @@
export const plannerSpeedControlsSource = " 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";
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const preferenceEventsSource = "$('appStatusRefreshBtn')?.addEventListener('click',loadAppStatus); $('portCheckEnabled')?.addEventListener('change',savePortCheckPref); $('portCheckNowBtn')?.addEventListener('click',()=>loadPortCheck(true)); $('bootstrapThemeSelect')?.addEventListener('change',saveAppearancePreferences); $('fontFamilySelect')?.addEventListener('change',saveAppearancePreferences); $('interfaceScaleRange')?.addEventListener('input',e=>applyInterfaceScale(e.target.value)); $('interfaceScaleRange')?.addEventListener('change',saveAppearancePreferences); $('compactTorrentListEnabled')?.addEventListener('change',saveAppearancePreferences); $('resetViewPreferencesBtn')?.addEventListener('click',resetViewPreferences); $('titleSpeedEnabled')?.addEventListener('change',saveTitleSpeedPreference); $('trackerFaviconsEnabled')?.addEventListener('change',saveTrackerFaviconsPreference); $('reverseDnsEnabled')?.addEventListener('change',saveReverseDnsPreference); $('automationToastsEnabled')?.addEventListener('change',saveNotificationPrefs); $('smartQueueToastsEnabled')?.addEventListener('change',saveNotificationPrefs); $('saveEasterEggPrefsBtn')?.addEventListener('click',saveEasterEggPrefs); $('easterEggEnabled')?.addEventListener('change',saveEasterEggPrefs); document.querySelectorAll('.disk-monitor-mode').forEach(input=>input.addEventListener('change',async e=>{ diskMonitorMode=e.target.value||'default'; if(diskMonitorMode==='selected' && !diskMonitorSelectedPath && diskMonitorPaths.length) diskMonitorSelectedPath=diskMonitorPaths[0]; renderDiskMonitorPaths(); await saveDiskMonitorPrefs(); })); $('diskMonitorSelectedPath')?.addEventListener('change',async e=>{ diskMonitorSelectedPath=e.target.value||''; if(diskMonitorSelectedPath) diskMonitorMode='selected'; renderDiskMonitorPaths(); await saveDiskMonitorPrefs(); }); $('addDiskPathBtn')?.addEventListener('click',async()=>{ const p=($('diskMonitorPathInput')?.value||'').trim(); if(!p) return; if(!diskMonitorPaths.includes(p)) diskMonitorPaths.push(p); if(!diskMonitorSelectedPath) diskMonitorSelectedPath=p; if(diskMonitorMode==='default') diskMonitorMode='selected'; if($('diskMonitorPathInput')) $('diskMonitorPathInput').value=''; renderDiskMonitorPaths(); await saveDiskMonitorPrefs(); }); $('diskMonitorPaths')?.addEventListener('click',async e=>{ const use=e.target.closest('.disk-path-select'); if(use){ diskMonitorSelectedPath=use.dataset.path||''; diskMonitorMode='selected'; renderDiskMonitorPaths(); await saveDiskMonitorPrefs(); return; } const btn=e.target.closest('.disk-path-remove'); if(!btn) return; diskMonitorPaths=diskMonitorPaths.filter(p=>p!==btn.dataset.path); if(diskMonitorSelectedPath===btn.dataset.path) diskMonitorSelectedPath=diskMonitorPaths[0]||''; if(diskMonitorMode==='selected' && !diskMonitorSelectedPath) diskMonitorMode='default'; renderDiskMonitorPaths(); await saveDiskMonitorPrefs(); }); $('saveFooterPrefsBtn')?.addEventListener('click',saveFooterPreferences);\n ";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const profileFormSource = " function profileFormPayload(){ return {id:$('profileId')?.value||null,name:$('profileName')?.value||'',scgi_url:$('profileUrl')?.value||'',timeout_seconds:$('profileTimeout')?.value||5,max_parallel_jobs:$('profileParallel')?.value||5,light_parallel_jobs:$('jobLightParallel')?.value||4,light_job_timeout_seconds:$('jobLightTimeout')?.value||300,heavy_job_timeout_seconds:$('jobHeavyTimeout')?.value||7200,pending_job_timeout_seconds:$('jobPendingTimeout')?.value||900,is_remote:$('profileRemote')?.checked}; }\n function renderProfileDiagnostics(d={}){ const box=$('profileDiagnosticsResult'); if(!box) return; const status=profileDiagnosticStatusLabel(d.status || (d.ok?'normal':'error')); const cls=profileDiagnosticStatusClass(status); const paths=d.base_paths||{}; const wp=d.write_permissions||{}; const disk=d.free_disk||{}; const firstDisk=Object.values(disk)[0]||{}; const cards=[['Status',`<span class=\"badge text-bg-${cls}\">${esc(status)}</span>`],['rTorrent',esc(d.version||'-')],['Library',esc(d.library_version||'-')],['Response',d.response_time_ms!=null?`${esc(d.response_time_ms)} ms`:'-'],['Slow threshold',d.slow_threshold_ms!=null?`${esc(d.slow_threshold_ms)} ms`:'-'],['Default path',esc(paths.default_directory||'-')],['CWD',esc(paths.cwd||'-')],['Write',esc(Object.values(wp)[0]||'-')],['Free disk',esc(firstDisk.free_h||firstDisk.error||'-')]]; box.classList.remove('text-muted'); box.innerHTML=`<div class=\"profile-diagnostics-grid\">${cards.map(([k,v])=>`<div class=\"profile-diagnostics-card\"><small>${esc(k)}</small><b>${v}</b></div>`).join('')}</div>${d.error?`<div class=\"alert alert-danger mt-2 mb-0\">${esc(d.error)}</div>`:''}`; }\n async function testProfilePayload(payload=null){ const p=payload||profileFormPayload(); const res=await post('/api/profiles/test', p); renderProfileDiagnostics(res.diagnostics||{}); return res.diagnostics||{}; }\n\n function resetProfileForm(){ if($('profileId')) $('profileId').value=''; if($('profileName')) $('profileName').value=''; if($('profileUrl')) $('profileUrl').value=''; if($('profileTimeout')) $('profileTimeout').value='5'; if($('profileParallel')) $('profileParallel').value='5'; if($('profileRemote')) $('profileRemote').checked=false; if($('profileFormTitle')) $('profileFormTitle').textContent='Add profile'; if($('saveProfileBtn')) $('saveProfileBtn').innerHTML='<i class=\"fa-solid fa-plus\"></i> Add profile'; $('cancelProfileEditBtn')?.classList.add('d-none'); }\n function editProfileForm(profile){ if(!profile) return; if($('profileId')) $('profileId').value=profile.id; if($('profileName')) $('profileName').value=profile.name||''; if($('profileUrl')) $('profileUrl').value=profile.scgi_url||''; if($('profileTimeout')) $('profileTimeout').value=profile.timeout_seconds||5; if($('profileParallel')) $('profileParallel').value=profile.max_parallel_jobs||5; if($('profileRemote')) $('profileRemote').checked=!!profile.is_remote; fillJobSettings(profile); if($('profileFormTitle')) $('profileFormTitle').textContent='Edit rTorrent profile'; if($('saveProfileBtn')) $('saveProfileBtn').innerHTML='<i class=\"fa-solid fa-floppy-disk\"></i> Save profile'; $('cancelProfileEditBtn')?.classList.remove('d-none'); $('profileName')?.focus(); }\n";
+1
View File
@@ -0,0 +1 @@
export const profileListSource = " function markActiveProfileRow(id){\n // Note: Keeps the active rTorrent profile frame in sync immediately after switching, before diagnostics refresh finishes.\n const activeId=String(id||'');\n document.querySelectorAll('#profileList .profile-row').forEach(row=>{\n const isActive=String(row.dataset.profileId||'')===activeId;\n row.classList.toggle('active', isActive);\n row.setAttribute('aria-current', isActive ? 'true' : 'false');\n const badge=row.querySelector('[data-active-profile-badge]');\n if(badge) badge.classList.toggle('d-none', !isActive);\n });\n }\n function profileDiagnosticStatusClass(status){\n // Note: rTorrent profile badges reuse Bootstrap colors and the same normal/slow/error idea as the poller panel.\n const value=String(status||'unknown').toLowerCase();\n if(value==='normal' || value==='online') return 'success';\n if(value==='slow' || value==='slowdown') return 'warning';\n if(value==='error' || value==='recovery') return 'danger';\n return 'secondary';\n }\n function profileDiagnosticStatusLabel(status){\n const value=String(status||'unknown').toLowerCase();\n return value==='online' ? 'normal' : value;\n }\n async function refreshProfiles(){ const j=await (await fetch('/api/profiles')).json(); profileCache=new Map((j.profiles||[]).map(p=>[String(p.id),p])); const active=String(j.active?.id ?? activeProfileId ?? ''); const rows=j.profiles||[]; const statusMap=new Map(); try{ const d=await (await fetch('/api/profiles/diagnostics')).json(); (d.diagnostics||[]).forEach(x=>statusMap.set(String(x.profile_id), x)); }catch(e){} $('profileList').innerHTML=rows.map(p=>{ const d=statusMap.get(String(p.id))||{}; const st=profileDiagnosticStatusLabel(d.status || 'unknown'); const cls=profileDiagnosticStatusClass(st); const response=d.response_time_ms?` · ${esc(d.response_time_ms)} ms`:''; const threshold=d.slow_threshold_ms?` · slow > ${esc(d.slow_threshold_ms)} ms`:''; const isActive=String(p.id)===active; return `<div class=\"profile-row ${isActive?'active':''}\" data-profile-id=\"${esc(p.id)}\" aria-current=\"${isActive?'true':'false'}\"><b>${esc(p.name)} <span data-active-profile-badge class='badge text-bg-primary ms-1 ${isActive?'':'d-none'}'>active</span> ${p.is_remote?\"<span class='badge text-bg-secondary ms-1'>remote</span>\":''} <span class=\"badge text-bg-${cls}\">${esc(st)}</span></b><span>${esc(p.scgi_url)} · heavy ${esc(p.max_parallel_jobs||5)} · light ${esc(p.light_parallel_jobs||4)} · poll ${esc(p.polling_min_interval_seconds||'-')}s${response}${threshold}</span><div class=\"profile-actions\"><button class=\"btn btn-xs btn-outline-primary\" data-use-profile=\"${p.id}\"><i class=\"fa-solid fa-plug-circle-check\"></i> use</button><button class=\"btn btn-xs btn-outline-info\" data-test-saved-profile=\"${p.id}\" title=\"Diagnostics\"><i class=\"fa-solid fa-stethoscope\"></i></button><button class=\"btn btn-xs btn-outline-secondary\" data-edit-profile=\"${p.id}\" title=\"Edit\"><i class=\"fa-solid fa-pen-to-square\"></i></button><button class=\"btn btn-xs btn-outline-danger\" data-del-profile=\"${p.id}\" title=\"Delete\"><i class=\"fa-solid fa-trash-can\"></i> Remove</button></div></div>`; }).join('')||'No profiles.'; }\n";
+1
View File
@@ -0,0 +1 @@
export const profileSelectionSource = " function renderProfileSelectionState(count=0){\n hasTorrentSnapshot = false;\n torrentSummary = {filters:{all:{count:0},downloading:{count:0},seeding:{count:0},paused:{count:0},checking:{count:0},error:{count:0},stopped:{count:0}}};\n torrents.clear();\n selected.clear();\n renderCounts();\n const body = $('torrentBody');\n if(body){\n body.innerHTML = `<tr><td colspan=\"${torrentColumnSpan()}\" class=\"empty\"><div class=\"empty-state\"><b>Select an rTorrent profile.</b><span>${esc(count)} profile(s) are configured for this trusted bypass session. Choose which one to open.</span><button id=\"chooseProfileBtn\" class=\"btn btn-sm btn-primary\" type=\"button\"><i class=\"fa-solid fa-server\"></i> Choose profile</button></div></td></tr>`;\n }\n const list = $('mobileList');\n if(list) list.innerHTML = `<div class=\"empty\"><div class=\"empty-state\"><b>Select an rTorrent profile.</b><span>Choose a profile to load torrents.</span></div></div>`;\n if($('detailPane')) $('detailPane').innerHTML = 'Choose an rTorrent profile to load details.';\n }\n\n async function openProfilePicker(){\n try{\n const j=await (await fetch('/api/profiles',{cache:'no-store'})).json();\n const select=$('profileSelect');\n if(select) select.innerHTML=(j.profiles||[]).map(p=>`<option value=\"${esc(p.id)}\" ${j.active?.id===p.id?'selected':''}>${esc(p.name)}</option>`).join('') || '<option value=\"\">No profiles configured</option>';\n }catch(e){}\n new bootstrap.Modal($('profilePickerModal')).show();\n }\n\n // Note: On trusted auth-bypass entry, existing profiles are not auto-selected; the visitor must choose the target profile.\n async function showFirstRunSetup(){\n if(hasActiveProfile || firstRunSetupShown) return;\n firstRunSetupShown = true;\n let profiles=[];\n try{\n const j=await (await fetch('/api/profiles',{cache:'no-store'})).json();\n if(j.active?.id){\n activeProfileId=j.active.id;\n hasActiveProfile=true;\n window.PYTORRENT.activeProfile=Number(j.active.id);\n return;\n }\n profiles=j.profiles||[];\n }catch(e){}\n $('connBadge').className='badge text-bg-warning';\n if(profiles.length){\n $('connBadge').textContent='select profile';\n setInitialLoader('Select rTorrent profile','Choose which configured rTorrent profile to open.');\n renderProfileSelectionState(profiles.length);\n hideInitialLoader();\n setTimeout(()=>openProfilePicker(), 120);\n return;\n }\n $('connBadge').textContent='setup required';\n setInitialLoader('Configure rTorrent','Add the first rTorrent profile to start loading torrents.');\n renderNoProfileState();\n hideInitialLoader();\n setTimeout(()=>{ activateToolTab('rtorrents'); new bootstrap.Modal($('toolsModal')).show(); }, 120);\n }\n";
+1
View File
@@ -0,0 +1 @@
export const ratioToolsSource = " async function loadRatios(){ const j=await (await fetch('/api/ratio-groups')).json(); const groups=j.groups||[], history=j.history||[]; if($('ratioAssignSelect')) $('ratioAssignSelect').innerHTML=groups.map(g=>`<option value=\"${esc(g.name)}\">${esc(g.name)} (${esc(g.min_ratio)}-${esc(g.max_ratio)})</option>`).join(''); if($('ratioManager')) $('ratioManager').innerHTML=`<h6>Groups</h6>${table(['Name','Min','Max','Seed min','Action','Move path','Set label','Enabled'],groups.map(g=>[esc(g.name),esc(g.min_ratio),esc(g.max_ratio),esc(g.seed_time_minutes||g.min_seed_time_minutes||0),esc(g.action),esc(g.move_path||''),esc(g.set_label||''),g.enabled?'yes':'no']))}<h6 class=\"mt-3\">Applied history</h6>${table(['Time','Torrent','Group','Action','Status','Reason'],history.map(h=>[humanDateCell(h.created_at),esc(h.torrent_name||h.torrent_hash),esc(h.group_name||''),esc(h.action),esc(h.status),esc(h.reason||'')]))}`; }\n $('labelModal')?.addEventListener('show.bs.modal',async()=>{ modalLabels=new Set(selectedHashes().flatMap(h=>labelNames(torrents.get(h)?.label))); if($('labelInput')) $('labelInput').value=''; await loadLabels(); renderLabelChooser(); });\n $('saveLabelBtn')?.addEventListener('click',async()=>{ const typed=($('labelInput')?.value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean); for(const l of typed){ modalLabels.add(l); await saveKnownLabel(l); } await runAction('set_label',{label:labelValue([...modalLabels])}); bootstrap.Modal.getInstance($('labelModal'))?.hide(); });\n $('addLabelToSelectionBtn')?.addEventListener('click',async()=>{ const typed=($('labelInput')?.value||'').split(/[,;|]+/).map(x=>x.trim()).filter(Boolean); for(const l of typed){ modalLabels.add(l); await saveKnownLabel(l); } if($('labelInput')) $('labelInput').value=''; renderLabelChooser(); });\n $('clearLabelsBtn')?.addEventListener('click',()=>{ modalLabels.clear(); renderLabelChooser(); });\n $('labelList')?.addEventListener('click',e=>{ const chip=e.target.closest('.label-chip'); if(!chip) return; const v=chip.dataset.label||''; modalLabels.has(v)?modalLabels.delete(v):modalLabels.add(v); renderLabelChooser(); });\n $('selectedLabelList')?.addEventListener('click',e=>{ const chip=e.target.closest('.label-selected'); if(!chip) return; modalLabels.delete(chip.dataset.label||''); renderLabelChooser(); });\n $('newLabelBtn')?.addEventListener('click',async()=>{ await saveKnownLabel($('newLabelName')?.value||''); if($('newLabelName')) $('newLabelName').value=''; });\n $('ratioAssignModal')?.addEventListener('show.bs.modal',loadRatios); $('applyRatioBtn')?.addEventListener('click',async()=>{ await runAction('set_ratio_group',{ratio_group:$('ratioAssignSelect').value}); bootstrap.Modal.getInstance($('ratioAssignModal'))?.hide(); }); $('ratioSaveBtn')?.addEventListener('click',async()=>{ await post('/api/ratio-groups',{name:$('ratioName').value,min_ratio:$('ratioMin').value,max_ratio:$('ratioMax').value,seed_time_minutes:$('ratioSeed').value,action:$('ratioAction').value,move_path:$('ratioMovePath')?.value||'',set_label:$('ratioSetLabel')?.value||'',ignore_private:$('ratioIgnorePrivate')?.checked!==false,ignore_active_upload:$('ratioIgnoreUpload')?.checked!==false}); loadRatios(); }); $('ratioCheckBtn')?.addEventListener('click',async()=>{ const j=await post('/api/ratio-groups/check',{}); toast(`Ratio applied ${j.result?.applied||0} torrent(s)`,'success'); loadRatios(); });\n";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const rssEventsSource = "$('rssFeedBtn')?.addEventListener('click',async()=>{await post('/api/rss/feeds',{id:$('rssFeedId')?.value||null,name:$('rssName').value,url:$('rssUrl').value,interval_minutes:$('rssInterval')?.value||30,enabled:true}); if($('rssFeedId')) $('rssFeedId').value=''; loadRss();}); $('rssRuleBtn')?.addEventListener('click',async()=>{await post('/api/rss/rules',{id:$('rssRuleId')?.value||null,name:$('rssRuleName').value,pattern:$('rssPattern').value,exclude_pattern:$('rssExclude')?.value||'',min_size_mb:$('rssMinSize')?.value||0,max_size_mb:$('rssMaxSize')?.value||0,category:$('rssCategory')?.value||'',quality:$('rssQuality')?.value||'',season:$('rssSeason')?.value||null,episode:$('rssEpisode')?.value||null,save_path:$('rssPath').value,label:$('rssLabel').value}); if($('rssRuleId')) $('rssRuleId').value=''; loadRss();}); $('rssTestBtn')?.addEventListener('click',async()=>{try{const j=await post('/api/rss/rules/test',{feed_url:$('rssUrl').value,rule:{pattern:$('rssPattern').value,exclude_pattern:$('rssExclude')?.value||'',min_size_mb:$('rssMinSize')?.value||0,max_size_mb:$('rssMaxSize')?.value||0,category:$('rssCategory')?.value||'',quality:$('rssQuality')?.value||'',season:$('rssSeason')?.value||null,episode:$('rssEpisode')?.value||null}}); $('rssTestResult').innerHTML=table(['Title','Reason'],(j.result?.matches||[]).map(x=>[esc(x.title),esc(x.reason)]));}catch(e){toast(e.message,'danger');}}); $('rssCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/rss/check',{}); toastMessage('toast.rssQueued','success',{queued:j.queued}); loadRss();}catch(e){toast(e.message,'danger');} finally{setBusy(false);}}); $('rssManager')?.addEventListener('click',async e=>{const ef=e.target.closest('.rss-edit-feed'); const er=e.target.closest('.rss-edit-rule'); const df=e.target.closest('.rss-delete-feed'); const dr=e.target.closest('.rss-delete-rule'); if(ef){const f=JSON.parse(ef.dataset.feed||'{}'); $('rssFeedId').value=f.id||''; $('rssName').value=f.name||''; $('rssUrl').value=f.url||''; $('rssInterval').value=f.interval_minutes||30;} if(er){const r=JSON.parse(er.dataset.rule||'{}'); $('rssRuleId').value=r.id||''; $('rssRuleName').value=r.name||''; $('rssPattern').value=r.pattern||''; $('rssExclude').value=r.exclude_pattern||''; $('rssMinSize').value=r.min_size_mb||''; $('rssMaxSize').value=r.max_size_mb||''; $('rssCategory').value=r.category||''; $('rssQuality').value=r.quality||''; $('rssSeason').value=r.season||''; $('rssEpisode').value=r.episode||''; $('rssPath').value=r.save_path||''; $('rssLabel').value=r.label||'';} if(df&&confirm('Delete RSS feed?')){await fetch(`/api/rss/feeds/${df.dataset.id}`,{method:'DELETE'}); loadRss();} if(dr&&confirm('Delete RSS rule?')){await fetch(`/api/rss/rules/${dr.dataset.id}`,{method:'DELETE'}); loadRss();}}); ";
+1
View File
@@ -0,0 +1 @@
export const rssToolsSource = "\n async function loadRss(){\n const j=await (await fetch('/api/rss')).json();\n const feeds=j.feeds||[], rules=j.rules||[], history=j.history||[];\n if($('rssManager')) $('rssManager').innerHTML=`<h6>Feeds</h6>${table(['Name','URL','Interval','Last check','Last error','Actions'],feeds.map(f=>[esc(f.name),esc(f.url),esc(f.interval_minutes||30)+' min',humanDateCell(f.last_checked_at),esc(f.last_error||''),`<button class=\"btn btn-xs btn-outline-primary rss-edit-feed\" data-feed='${esc(JSON.stringify(f))}'><i class=\"fa-solid fa-pen-to-square\"></i> Edit</button> <button class=\"btn btn-xs btn-outline-danger rss-delete-feed\" data-id=\"${esc(f.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button>`]))}<h6 class=\"mt-3\">Rules</h6>${table(['Name','Include','Exclude','Filters','Path','Label','Actions'],rules.map(r=>[esc(r.name),esc(r.pattern),esc(r.exclude_pattern||''),esc([r.min_size_mb?`min ${r.min_size_mb}MB`:'',r.max_size_mb?`max ${r.max_size_mb}MB`:'',r.category,r.quality,r.season?`S${r.season}`:'',r.episode?`E${r.episode}`:''].filter(Boolean).join(', ')),esc(r.save_path),esc(r.label),`<button class=\"btn btn-xs btn-outline-primary rss-edit-rule\" data-rule='${esc(JSON.stringify(r))}'><i class=\"fa-solid fa-pen-to-square\"></i> Edit</button> <button class=\"btn btn-xs btn-outline-danger rss-delete-rule\" data-id=\"${esc(r.id)}\"><i class=\"fa-solid fa-trash-can\"></i> Delete</button>`]))}<h6 class=\"mt-3\">RSS log</h6>${table(['Time','Title','Status','Message'],history.map(h=>[humanDateCell(h.created_at),esc(h.title||h.link||''),esc(h.status),esc(h.message||'')]))}`;\n }\n\n";
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const smartQueueEventsSource = "$('smartRefillMode')?.addEventListener('change',updateSmartRefillControls); $('smartSaveBtn')?.addEventListener('click',saveSmartQueue); $('smartCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/smart-queue/check',{}); if(j.queued){toastMessage('toast.smartQueueCheckQueued','success'); await loadJobs().catch(()=>{}); await loadSmartQueue(); return;} const r=j.result||{}; if(j.torrent_patch) patchRows(j.torrent_patch); toast(smartQueueToastMessage(r),'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();}); ";
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const speedLimitControlsSource = "const mbpsToKib=mbps=>mbps?Math.round((Number(mbps)*1000000/8)/1024):0;\n const kibToMbps=kib=>kib?Math.round((Number(kib)*1024*8)/1000000):0;\n function setLimitSliderMax(slider,mbps){ if(slider && mbps>Number(slider.max||0)) slider.max=String(mbps); }\n function setLimitValue(targetId,kib){ const input=$(targetId); if(input) input.value=Math.max(0,Math.round(Number(kib)||0)); }\n function updateLimitSlider(slider){ if(!slider) return; const input=$(slider.dataset.target); const out=$(slider.dataset.output); const mbps=kibToMbps(Number(input?.value||0)); setLimitSliderMax(slider,mbps); slider.value=String(mbps); if(out) out.textContent=mbps?`${mbps} Mbit/s`:'Unlimited'; }\n function updateLimitSliders(){ document.querySelectorAll('.limit-slider').forEach(updateLimitSlider); }\n function syncLimitInputFromSlider(slider){ const mbps=Number(slider.value||0); setLimitValue(slider.dataset.target,mbpsToKib(mbps)); updateLimitSlider(slider); }\n document.querySelectorAll('.limit-preset').forEach(b=>b.addEventListener('click',()=>{const kib=mbpsToKib(Number(b.dataset.mbps||0));setLimitValue('limitDown',kib);setLimitValue('limitUp',kib);updateLimitSliders();}));\n document.querySelectorAll('.limit-slider').forEach(slider=>slider.addEventListener('input',()=>syncLimitInputFromSlider(slider)));\n ['limitDown','limitUp'].forEach(id=>$(id)?.addEventListener('input',updateLimitSliders));\n $('saveSpeedBtn')?.addEventListener('click',async()=>{const btn=$('saveSpeedBtn');buttonBusy(btn,true);setBusy(true);try{await post('/api/speed/limits',{down:Math.round(Number($('limitDown').value||0)*1024),up:Math.round(Number($('limitUp').value||0)*1024)});toast('Speed limits queued','success');bootstrap.Modal.getInstance($('speedModal'))?.hide();}catch(e){toast(e.message,'danger');}finally{buttonBusy(btn,false);setBusy(false);}}); $('speedModal')?.addEventListener('show.bs.modal',()=>{setLimitValue('limitDown',lastLimits.down?Math.round(lastLimits.down/1024):0);setLimitValue('limitUp',lastLimits.up?Math.round(lastLimits.up/1024):0);updateLimitSliders();});\n // Note: rTorrent profile management was moved to profiles.js so poller.js only keeps polling and tools wiring.\n ";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const systemStatsSocketSource = " socket.on('system_stats',s=>{\n const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined;\n $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);\n $('statRamBox')?.classList.toggle('d-none',!usageAvailable);\n $('systemChart')?.classList.toggle('d-none',!usageAvailable);\n if(usageAvailable){\n $('statCpu').textContent=s.cpu??'-';\n $('statRam').textContent=s.ram??'-';\n drawSystemUsage(s.cpu,s.ram);\n }\n $('statVersion').textContent=s.version||'-';\n applyLiveSpeedStats(s);\n lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};\n $('statDlLimit').textContent=s.down_limit_h||'∞';\n $('statUlLimit').textContent=s.up_limit_h||'∞';\n $('statTotalDl').textContent=compactTransferText(s.total_down_h);\n $('statTotalUl').textContent=compactTransferText(s.total_up_h);\n updateSpeedPeaks(s.speed_peaks||{});\n drawTraffic(s.down_rate,s.up_rate);\n if(diskMonitorMode==='default'){\n drawDiskUsage(s.disk);\n }else{\n refreshUserDiskUsage(false);\n }\n updateRtorrentFooterStats(s, false);\n saveFooterStatusCache(s);\n if(s.poller) fillPoller(null,s.poller);\n applyFooterPreferences();\n });\n";
@@ -0,0 +1 @@
export const themeMobileControlsSource = "$('themeToggle')?.addEventListener('click',async()=>{const cur=document.documentElement.dataset.bsTheme==='dark'?'light':'dark';document.documentElement.dataset.bsTheme=cur;await post('/api/preferences',{theme:cur}).catch(()=>{});}); $('mobileToggle')?.addEventListener('click',()=>{document.body.classList.toggle('mobile-mode-manual');syncMobileMode();}); window.addEventListener('resize',()=>syncMobileMode(),{passive:true}); syncMobileMode();\n";
+1
View File
@@ -0,0 +1 @@
export const toolPaneEventsSource = "function switchAppStatusPane(pane){ document.querySelectorAll('#appStatusTabs [data-appstatus-pane], #appStatusManager [data-appstatus-pane]').forEach(x=>x.classList.toggle('active',x.dataset.appstatusPane===pane)); $('appStatusManager')?.querySelectorAll('[data-appstatus-panel]').forEach(x=>x.classList.toggle('d-none',x.dataset.appstatusPanel!==pane)); } $('appStatusTabs')?.addEventListener('click',e=>{ const tab=e.target.closest('[data-appstatus-pane]'); if(tab) switchAppStatusPane(tab.dataset.appstatusPane); }); $('appStatusManager')?.addEventListener('click',e=>{ const tab=e.target.closest('[data-appstatus-pane]'); if(tab) switchAppStatusPane(tab.dataset.appstatusPane); }); $('healthDashboardManager')?.addEventListener('click',e=>{ const tab=e.target.closest('[data-health-pane]'); if(tab && typeof setHealthPane==='function') setHealthPane(tab.dataset.healthPane); }); $('torrentStatsManager')?.addEventListener('click',e=>{ const tab=e.target.closest('[data-torrentstats-pane]'); if(tab && typeof setTorrentStatsPane==='function') setTorrentStatsPane(tab.dataset.torrentstatsPane); }); $('torrentStatsRefreshBtn')?.addEventListener('click',()=>loadTorrentStats(true)); $('authUserSaveBtn')?.addEventListener('click',saveAuthUser); $('authUserCancelBtn')?.addEventListener('click',resetAuthUserForm); $('authUsersManager')?.addEventListener('click',async e=>{ const edit=e.target.closest('.auth-edit'); const token=e.target.closest('.auth-token:not(.auth-token-list)'); const tokenList=e.target.closest('.auth-token-list'); const del=e.target.closest('.auth-delete'); if(edit){ editAuthUser(JSON.parse(edit.dataset.user||'{}')); return; } if(token){ await generateAuthToken(token.dataset.id); return; } if(tokenList){ await showAuthTokens(tokenList.dataset.id); return; } if(del && confirm('Delete user?')){ try{ const j=await post(`/api/auth/users/${del.dataset.id}`,{},'DELETE'); if(!j.ok) throw new Error(j.error||'Delete failed'); toast('User deleted','success'); await loadAuthUsers(); }catch(e){ toast(e.message,'danger'); } } }); ";
+1
View File
@@ -0,0 +1 @@
export const toolsModalSource = "ensurePlannerToolsUI(); try{const j=await fetch('/api/poller/settings').then(r=>r.json()); fillPoller(j.settings||{},j.runtime||{});}catch(e){} }\n async function savePollerSettings(){ try{const j=await post('/api/poller/settings',pollerPayload()); fillPoller(j.settings||pollerPayload(),null); toast('Poller settings saved','success');}catch(e){toast(e.message,'danger');} }\n ensurePlannerToolsUI(); ensureDashboardToolsUI(); loadDownloadPlanner(); $('toolsModal')?.addEventListener('show.bs.modal',()=>{ensurePlannerToolsUI();ensureDashboardToolsUI();refreshProfiles();loadLabels();loadRatios();loadRss();loadSmartQueue();loadRtConfig();loadAutomations();loadCleanup();loadBackup();loadAppStatus();loadOperationLogs();renderHealthDashboard();renderSmartViewsManager();renderNotificationCenter();loadPreferences();loadJobSettings();if(document.querySelector('.tool-tab[data-tool=\"users\"]')?.classList.contains('active')) loadAuthUsers();loadDownloadPlanner();loadPollerSettings();renderColumnManager();applyColumnVisibility();updateAutomationForm();}); const toolPanelIds={rtorrents:'toolRtorrents',settings:'toolRtorrents',torrentstats:'toolTorrentStats',preferences:'toolPreferences',jobs:'toolJobs',users:'toolUsers',labels:'toolLabels',ratio:'toolRatio',rss:'toolRss',columns:'toolColumns',smart:'toolSmart',automations:'toolAutomations',rtconfig:'toolRtconfig',cleanup:'toolCleanup',backup:'toolBackup',logs:'toolLogs',appstatus:'toolAppstatus',planner:'toolPlanner',poller:'toolPoller',smartviews:'toolSmartviews',notifications:'toolNotifications'}; 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==='torrentstats') loadTorrentStats(false); if(tool==='appstatus') loadAppStatus(); if(tool==='cleanup') loadCleanup(); if(tool==='backup') loadBackup(); if(tool==='preferences') loadPreferences(); if(tool==='jobs') loadJobSettings(); if(tool==='logs') loadOperationLogs(true); if(tool==='users') loadAuthUsers(); if(tool==='planner') loadDownloadPlanner(); if(tool==='poller') loadPollerSettings(); if(tool==='smartviews') renderSmartViewsManager(); if(tool==='notifications') renderNotificationCenter(); if(tool==='diagnostics') loadAppStatus(); }; document.querySelectorAll('.tool-tab').forEach(b=>b.addEventListener('click',()=>activateToolTab(b.dataset.tool||'rtorrents'))); bindOperationLogEvents(); ";
@@ -0,0 +1 @@
export const torrentActionStateSource = " function actionLabel(action){\n // Note: These labels are shown inside a torrent row, so they stay short and do not repeat the word torrent.\n const labels={start:'Start',pause:'Pause',stop:'Stop',resume:'Resume',recheck:'Check',reannounce:'Reannounce',remove:'Remove',move:'Move',set_label:'Set label',set_ratio_group:'Set ratio'};\n return labels[action] || `Working: ${action}`;\n }\n function actionIcon(action){\n return ({start:'fa-play',pause:'fa-pause',stop:'fa-stop',resume:'fa-play',recheck:'fa-rotate',reannounce:'fa-bullhorn',remove:'fa-trash',move:'fa-folder-open',set_label:'fa-tag',set_ratio_group:'fa-scale-balanced'}[action]) || 'fa-gears';\n }\n function markTorrentOperation(hashes, action, jobId, state='queued'){\n const label=actionLabel(action);\n [...new Set(hashes||[])].filter(Boolean).forEach(hash=>activeOperations.set(hash,{action,jobId,state,label,updatedAt:Date.now()}));\n scheduleRender(true);\n }\n function markQueuedJobs(response, fallbackHashes, action){\n // Note: Supports API responses that split one large user action into multiple queued bulk parts.\n const jobs=Array.isArray(response?.jobs)?response.jobs:[];\n if(jobs.length){ jobs.forEach(job=>markTorrentOperation(job.hashes||[],action,job.job_id,'queued')); return; }\n markTorrentOperation(fallbackHashes,action,response?.job_id,'queued');\n }\n function clearJobOperation(jobId, hashes=[]){\n if(jobId){ [...activeOperations].forEach(([hash,op])=>{ if(op.jobId===jobId) activeOperations.delete(hash); }); }\n (hashes||[]).forEach(hash=>activeOperations.delete(hash));\n scheduleRender(true);\n }\n function actionCompletionPatch(action, torrent){\n // Note: rTorrent can acknowledge light state actions before the next list read exposes the new status.\n const complete=Number(torrent?.complete||0) !== 0;\n if(['start','resume','unpause'].includes(action)) return {state:1, active:1, paused:false, post_check:false, status:complete?'Seeding':'Downloading'};\n if(action==='pause') return {state:1, active:0, paused:true, status:'Paused'};\n if(action==='stop') return {state:0, active:0, paused:false, post_check:false, status:'Stopped'};\n return null;\n }\n function applyActionCompletionState(action, hashes=[]){\n // Note: This optimistic patch keeps completed light actions visible while delayed cache refreshes settle.\n const unique=[...new Set(hashes||[])].filter(Boolean);\n let changed=false;\n unique.forEach(hash=>{\n const current=torrents.get(hash);\n const patch=actionCompletionPatch(action,current);\n if(!current || !patch) return;\n torrents.set(hash,{...current,...patch});\n changed=true;\n });\n if(changed) scheduleRender(true);\n }\n function activeOperationFor(t){ return activeOperations.get(t.hash) || null; }\n";
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const torrentDetailsLoaderSource = " async function loadDetails(tab, options={}){\n const t=torrents.get(selectedHash);\n const silent = !!options.silent;\n if(tab !== 'files') clearFilesAutoRefresh();\n if(tab !== 'peers') clearReverseDnsPeerRefresh();\n if($('peersRefreshBox')) $('peersRefreshBox').classList.toggle('d-none', tab!=='peers');\n setupPeersRefresh(tab);\n if(!t) return;\n if(tab==='general') return renderGeneral();\n if(tab==='log'){\n $('detailPane').innerHTML=`<pre class=\"torrent-log-message\">${esc(t.message||'No logs')}</pre>`;\n return;\n }\n const pane=$('detailPane');\n if(!silent) pane.innerHTML=`<div class=\"loading-line\"><span class=\"spinner-border spinner-border-sm\"></span> Loading ${esc(tab)}...</div>`;\n try{\n const detailUrl = tab==='chunks' ? `/api/torrents/${encodeURIComponent(selectedHash)}/chunks?max_cells=${chunkMaxCellsForDensity()}` : `/api/torrents/${encodeURIComponent(selectedHash)}/${tab}`;\n const res=await fetch(detailUrl,{headers:{'Accept':'application/json'}});\n const text=await res.text();\n let json;\n try{\n json=JSON.parse(text);\n }catch(parseErr){\n throw new Error(`Invalid API response for ${tab}. HTTP ${res.status}`);\n }\n if(!res.ok || !json.ok) throw new Error(json.error||`HTTP ${res.status}`);\n if(tab!==activeTab()) return;\n if(tab==='files') renderFiles(json.files||[]);\n if(tab==='chunks') renderChunks(json.chunks||{});\n if(tab==='peers') renderPeers(json.peers||[]);\n if(tab==='trackers') renderTrackers(json.trackers||[]);\n }catch(e){\n if(!silent) pane.innerHTML=`<div class=\"text-danger\">${esc(e.message)}</div>`;\n }\n }\n";
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const torrentFilterHelpersSource = " // Note: Displays status filter summaries calculated and cached by the backend API.\n const FILTER_COUNT_IDS = {all:'countAll', downloading:'countDownloading', seeding:'countSeeding', paused:'countPaused', checking:'countChecking', error:'countError', post_check:'countPostCheck', stopped:'countStopped', moving:'countMoving'};\n function formatFilterBytes(value){ return fmtBytes(value).replace(/\\.0 (?=GiB|TiB)/, ' '); }\n function filterMetaLine(bucket){\n if(!bucket || !Number(bucket.count||0)) return '';\n const disk=Number(bucket.disk_bytes ?? bucket.completed_bytes ?? 0);\n return `Data ${formatFilterBytes(disk)}`;\n }\n function filterNeedsDownloadDetails(type, bucket){\n if(!bucket || !Number(bucket.count||0)) return false;\n if(type==='downloading' || type==='post_check') return true;\n if(type!=='paused' && type!=='stopped') return false;\n const size=Number(bucket.size||0);\n const completed=Number(bucket.completed_bytes ?? bucket.disk_bytes ?? 0);\n const remaining=Number(bucket.remaining_bytes ?? Math.max(0, size-completed));\n const progress=Number(bucket.progress_percent ?? (size ? (completed / size) * 100 : 0));\n return size > 0 && remaining > 0 && progress < 100;\n }\n function filterTooltipLine(bucket, type){\n if(!bucket || !Number(bucket.count||0)) return '';\n const size=Number(bucket.size||0);\n const disk=Number(bucket.disk_bytes ?? bucket.completed_bytes ?? 0);\n const completed=Number(bucket.completed_bytes ?? disk);\n const remaining=Number(bucket.remaining_bytes ?? Math.max(0, size-completed));\n const progress=Number(bucket.progress_percent ?? (size ? (completed / size) * 100 : 0));\n const left=Number(bucket.remaining_percent ?? Math.max(0, 100-progress));\n const lines=[`Data: ${formatFilterBytes(disk)}`];\n if(filterNeedsDownloadDetails(type, bucket)){\n lines.push(`Total to download: ${formatFilterBytes(size)}`);\n lines.push(`Downloaded: ${formatFilterBytes(completed)} (${progress.toFixed(1)}%)`);\n lines.push(`Left: ${formatFilterBytes(remaining)} (${left.toFixed(1)}%)`);\n }\n return lines.join('\\n');\n }\n function applyFilterTooltip(button, tooltip, ariaLabel){\n if(tooltip){\n button.title = tooltip;\n button.setAttribute('aria-label', ariaLabel);\n } else {\n button.removeAttribute('title');\n button.removeAttribute('aria-label');\n }\n }\n function ensureStableFilterTooltip(button){\n if(filterTooltipState.has(button)) return filterTooltipState.get(button);\n const state = {hovering:false, pending:null};\n filterTooltipState.set(button, state);\n button.addEventListener('mouseenter', () => {\n state.hovering = true;\n state.pending = null;\n });\n button.addEventListener('mouseleave', () => {\n state.hovering = false;\n if(state.pending){\n applyFilterTooltip(button, state.pending.tooltip, state.pending.ariaLabel);\n state.pending = null;\n }\n });\n return state;\n }\n // Note: Freezes tooltip content during hover; the next hover receives the newest live summary.\n function setStableFilterTooltip(button, tooltip, ariaLabel){\n const state = ensureStableFilterTooltip(button);\n if(state.hovering){\n state.pending = {tooltip, ariaLabel};\n return;\n }\n applyFilterTooltip(button, tooltip, ariaLabel);\n }\n";
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const torrentGeneralDetailsSource = " function formatDateTime(seconds){ const n=Number(seconds||0); if(!n) return '-'; try{ return new Date(n*1000).toLocaleString(); }catch(e){ return '-'; } }\n function joinRemotePath(base,name){\n const b=String(base||'').trim();\n const n=String(name||'').trim();\n if(!b && !n) return '-';\n if(!n) return b || '-';\n if(!b) return n;\n return `${b.replace(/\\/+$/,'')}/${n.replace(/^\\/+/,'')}`;\n }\n function renderGeneral(){\n const t=torrents.get(selectedHash);\n if(!t){ $('detailPane').innerHTML='Select a torrent.'; return; }\n const labels=labelNames(t.label).map(l=>`<span class=\"chip label-mini\"><i class=\"fa-solid fa-tag\"></i> ${esc(l)}</span>`).join(' ') || '<span class=\"text-muted\">-</span>';\n const ratioGroup=t.ratio_group ? `<span class=\"badge text-bg-info\">${esc(t.ratio_group)}</span>` : '<span class=\"text-muted\">Not assigned</span>';\n const statusClass=t.status==='Seeding'?'success':t.status==='Downloading'?'primary':t.status==='Checking'?'warning':t.status==='Paused'?'secondary':t.status==='Stopped'?'dark':'secondary';\n const fullPath=joinRemotePath(t.path,t.name);\n const cards=[\n ['Size', esc(t.size_h||'-')],\n ['Downloaded', esc(t.down_total_h||'-')],\n ['Uploaded', esc(t.up_total_h||'-')],\n ['Ratio', esc(t.ratio??'-')],\n ['Download speed', esc(t.down_rate_h||'-')],\n ['Upload speed', esc(t.up_rate_h||'-')],\n ['Seeds / Peers', `${esc(t.seeds??0)} / ${esc(t.peers??0)}`],\n ['ETA', esc(t.eta_h||'-')],\n ['Created', esc(formatDateTime(t.created))],\n ['Last activity', esc(formatDateTime(t.last_activity))],\n ['Priority', esc(t.priority??'-')],\n ].map(([label,value])=>`<div class=\"general-stat\"><b>${label}</b><span>${value}</span></div>`).join('');\n $('detailPane').innerHTML=`\n <section class=\"general-summary\">\n <div class=\"general-summary-main\">\n <div class=\"general-title-row\"><h6>${esc(t.name||'-')}</h6><span class=\"badge text-bg-${statusClass}\">${esc(t.status||'-')}</span></div>\n <div class=\"general-path\"><b>Directory</b><span>${esc(t.path||'-')}</span></div>\n <div class=\"general-path\"><b>Full data path</b><span>${esc(fullPath)}</span></div>\n </div>\n <div class=\"general-summary-side\"><b>Hash</b><code>${esc(t.hash||'-')}</code></div>\n </section>\n <div class=\"general-grid\">${cards}</div>\n <div class=\"general-meta\"><div><b>Labels</b><span>${labels}</span></div><div><b>Ratio rule</b><span>${ratioGroup}</span></div><div><b>Message</b><span>${esc(t.message||'-')}</span></div></div>`;\n }\n";
@@ -0,0 +1 @@
export const torrentPeerDetailsSource = " function peerBadges(p){\n const badges=[];\n if(p.encrypted) badges.push('<span class=\"badge text-bg-success\">enc</span>');\n if(p.incoming) badges.push('<span class=\"badge text-bg-info\">in</span>');\n if(p.snubbed) badges.push('<span class=\"badge text-bg-warning\">snub</span>');\n if(p.banned) badges.push('<span class=\"badge text-bg-danger\">ban</span>');\n return badges.join(' ') || '<span class=\"text-muted\">-</span>';\n }\n function peerHostCell(p){\n const host=String(p.host||'').trim();\n // Note: Hostnames use the available peer-table space instead of a fixed character-style cap.\n if(host) return `<span class=\"peer-host\" title=\"${esc(host)}\">${esc(host)}</span>`;\n if(p.host_pending) return '<span class=\"text-muted\">resolving</span>';\n return '<span class=\"text-muted\">-</span>';\n }\n function hasPendingReverseDns(peers){\n return reverseDnsEnabled && (peers||[]).some(p=>p && p.host_pending);\n }\n function clearReverseDnsPeerRefresh(){\n clearTimeout(reverseDnsRefreshTimer);\n reverseDnsRefreshTimer=null;\n reverseDnsRefreshInFlight=false;\n reverseDnsRefreshAttempts=0;\n reverseDnsRefreshHash=null;\n }\n function scheduleReverseDnsPeerRefresh(peers){\n // Note: PTR results are checked on a short independent loop, not on the manual/auto peers refresh interval.\n if(!hasPendingReverseDns(peers)){ clearReverseDnsPeerRefresh(); return; }\n if(activeTab()!=='peers' || !selectedHash) return;\n const hash=selectedHash;\n if(reverseDnsRefreshHash!==hash){ reverseDnsRefreshHash=hash; reverseDnsRefreshAttempts=0; }\n if(reverseDnsRefreshTimer || reverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n reverseDnsRefreshAttempts+=1;\n reverseDnsRefreshTimer=setTimeout(async()=>{\n reverseDnsRefreshTimer=null;\n if(activeTab()!=='peers' || selectedHash!==hash) return;\n reverseDnsRefreshInFlight=true;\n try{ await loadDetails('peers',{silent:true}); }\n finally{ reverseDnsRefreshInFlight=false; }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function renderPeers(peers){\n const headers=['Flag','IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Country','City','Client','%','DL','UL','Port','Flags');\n const rows=(peers||[]).map(p=>{\n const row=[flag(p.country_iso),`<span class=\"peer-ip\">${esc(p.ip)}<a class=\"peer-ip-link\" href=\"https://ipinfo.io/${encodeURIComponent(p.ip||'')}\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open IP info\"><i class=\"fa-solid fa-link\"></i></a></span>`];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(p.country),esc(p.city),esc(p.client),progressBar(p.completed,'peer-progress peer-progress-wide'),esc(p.down_rate_h),esc(p.up_rate_h),esc(p.port),peerBadges(p));\n return row;\n });\n $('detailPane').innerHTML=responsiveTable(headers,rows,reverseDnsEnabled ? 'peers-table peers-table-hosts' : 'peers-table');\n scheduleReverseDnsPeerRefresh(peers);\n }\n";
@@ -0,0 +1 @@
export const torrentRowRendererSource = " function statusMeta(t){\n const op=activeOperationFor(t);\n if(op) return {cls:'text-bg-info operation-status-badge', icon:actionIcon(op.action), color:'text-info', label:op.label};\n const status=String(t.status||'').toLowerCase();\n if(t.paused || status==='paused') return {cls:'text-bg-warning', icon:'fa-pause', color:'text-warning'};\n if(status==='checking' || Number(t.hashing||0)>0) return {cls:'text-bg-info', icon:'fa-rotate', color:'text-info'};\n if(status==='post-check' || t.post_check) return {cls:'text-bg-dark', icon:'fa-clipboard-check', color:'text-secondary', label:'Post-check'};\n if(status==='seeding') return {cls:'text-bg-success', icon:'fa-seedling', color:'text-success'};\n if(status==='downloading') return {cls:'text-bg-primary', icon:'fa-download', color:'text-primary'};\n if(status==='stopped') return {cls:'text-bg-secondary', icon:'fa-stop', color:'text-secondary'};\n return t.state ? {cls:'text-bg-success', icon:'fa-play', color:'text-success'} : {cls:'text-bg-secondary', icon:'fa-circle', color:'text-secondary'};\n }\n function statusBadge(t){ const m=statusMeta(t); return `<span class=\"badge status-badge ${m.cls}\"><i class=\"fa-solid ${m.icon} me-1\"></i>${esc(m.label || t.status)}</span>`; }\n function torrentWarning(t){ const msg=String(t.message||'').trim(); if(!msg) return null; const l=msg.toLowerCase(); const patterns=['error','failed','failure','timeout','timed out','tracker','could not','cannot','refused','unreachable','denied']; return patterns.some(p=>l.includes(p)) ? msg : null; }\n function torrentNameIcon(t){ const m=statusMeta(t); return `<i class=\"fa-solid ${m.icon} ${m.color}\"></i>`; }\n function boolCell(value){ return Number(value||0) ? '<span class=\"badge text-bg-success\">yes</span>' : '<span class=\"badge text-bg-secondary\">no</span>'; }\n function renderRow(t){\n const labels=labelNames(t.label).map(l=>`<span class=\"chip label-mini\"><i class=\"fa-solid fa-tag\"></i> ${esc(l)}</span>`).join(' ');\n const warn=torrentWarning(t);\n const op=activeOperationFor(t);\n const classes=[selected.has(t.hash)?'selected':'', t.paused?'torrent-paused':'', op?'torrent-operating':'', warn?'torrent-warning':''].filter(Boolean).join(' ');\n const title=[t.name,warn,op?op.label:''].filter(Boolean).join('\\n');\n return `<tr data-hash=\"${esc(t.hash)}\" class=\"${classes}\">`+\n `<td data-col=\"select\" class=\"sel\"><input class=\"row-check\" type=\"checkbox\" ${selected.has(t.hash)?'checked':''}></td>`+\n `<td data-col=\"name\" class=\"name\" title=\"${esc(title)}\">${warn?'<i class=\"fa-solid fa-triangle-exclamation torrent-warning-icon\"></i> ':''}${torrentNameIcon(t)} ${esc(t.name)}</td>`+\n `<td data-col=\"status\">${statusBadge(t)}</td>`+\n `<td data-col=\"size\">${esc(t.size_h)}</td>`+\n `<td data-col=\"progress\">${progress(t)}</td>`+\n `<td data-col=\"down_rate\">${esc(t.down_rate_h)}</td>`+\n `<td data-col=\"up_rate\">${esc(t.up_rate_h)}</td>`+\n `<td data-col=\"eta\">${esc(t.eta_h||\"-\")}</td>`+\n `<td data-col=\"seeds\">${esc(t.seeds)}</td>`+\n `<td data-col=\"peers\">${esc(t.peers)}</td>`+\n `<td data-col=\"ratio\">${esc(t.ratio)}</td>`+\n `<td data-col=\"path\" class=\"path\" title=\"${esc(t.path)}\">${esc(t.path)}</td>`+\n `<td data-col=\"label\">${labels||'<span class=\"text-muted\">-</span>'}</td>`+\n `<td data-col=\"ratio_group\">${esc(t.ratio_group||'')}</td>`+\n `<td data-col=\"down_total\">${esc(t.down_total_h||'-')}</td>`+\n `<td data-col=\"to_download\">${esc(t.to_download_h||'-')}</td>`+\n `<td data-col=\"up_total\">${esc(t.up_total_h||'-')}</td>`+\n `<td data-col=\"created\">${esc(formatDateTime(t.created))}</td>`+\n `<td data-col=\"last_activity\">${esc(formatDateTime(t.last_activity))}</td>`+\n `<td data-col=\"priority\">${esc(t.priority ?? '-')}</td>`+\n `<td data-col=\"state\">${boolCell(t.state)}</td>`+\n `<td data-col=\"active\">${boolCell(t.active)}</td>`+\n `<td data-col=\"complete\">${boolCell(t.complete)}</td>`+\n `<td data-col=\"hashing\">${esc(t.hashing ?? 0)}</td>`+\n `<td data-col=\"message\" class=\"message\" title=\"${esc(t.message||'')}\">${compactCell(t.message||'', 80)}</td>`+\n `<td data-col=\"hash\"><code>${esc(t.hash||'')}</code></td>`+\n `</tr>`;\n }\n\n\n\n\n";
@@ -0,0 +1 @@
export const torrentSelectionEventsSource = "function awaitMaybeRun(action){ runAction(action).catch?.(()=>{}); }\n function openRemoveModalForCurrentSelection(){\n // Note: Mobile remove uses the same Bootstrap modal as desktop, including the Remove with data switch.\n const modal=$('removeModal');\n if(!modal) return toast('Remove dialog is unavailable','danger');\n new bootstrap.Modal(modal).show();\n }\n document.addEventListener('click',e=>{ const ctx=$('ctxMenu'); if(!e.target.closest('#ctxMenu')) ctx.style.display='none'; const mobileFilter=e.target.closest('#mobileFilterBar .mobile-filter'); if(mobileFilter){ const key=mobileFilter.dataset.filter||'all'; if(key.startsWith('tracker:')){ activeTrackerFilter=key.slice(8); activeFilter='all'; mobileActiveFilterKey=key; } else { activeTrackerFilter=''; activeFilter=key; mobileActiveFilterKey=key; } syncFilterButtons(); saveActiveFilterPreference(); if($('tableWrap'))$('tableWrap').scrollTop=0; if($('mobileList'))$('mobileList').scrollTop=0; scheduleRender(true); return; } const mobileSort=e.target.closest('#mobileSortCycle'); if(mobileSort){ cycleMobileSort(); return; } const mobileSelectAll=e.target.closest('#mobileSelectAll'); if(mobileSelectAll){ toggleMobileVisibleSelection(); scheduleRender(true); return; } const mobileClear=e.target.closest('#mobileClearSelection'); if(mobileClear){ selected.clear(); selectedHash=null; lastSelectedHash=null; scheduleRender(true); return; } const mobileTorrentDownload=e.target.closest('#mobileBulkTorrentDownload'); if(mobileTorrentDownload){ downloadTorrentFiles(); return; } const mobileDetails=e.target.closest('.mobile-details-btn'); if(mobileDetails){ const card0=mobileDetails.closest('.mobile-card'); if(card0?.dataset.hash) openMobileDetails(card0.dataset.hash); return; } const mobileAct=e.target.closest('.mobile-card [data-action]'); if(mobileAct){ const card0=mobileAct.closest('.mobile-card'); selected.clear(); selected.add(card0.dataset.hash); selectedHash=card0.dataset.hash; lastSelectedHash=selectedHash; if(mobileAct.dataset.action==='remove') openRemoveModalForCurrentSelection(); else awaitMaybeRun(mobileAct.dataset.action); scheduleRender(true); return; } const mobileModal=e.target.closest('.mobile-card [data-mobile-modal]'); if(mobileModal){ const card0=mobileModal.closest('.mobile-card'); selected.clear(); selected.add(card0.dataset.hash); selectedHash=card0.dataset.hash; lastSelectedHash=selectedHash; scheduleRender(true); if(mobileModal.dataset.mobileModal==='label') new bootstrap.Modal($('labelModal')).show(); return; } const card=e.target.closest('.mobile-card'); const tr=e.target.closest('tr[data-hash]'); const row=tr||card; if(row){ const h=row.dataset.hash; const additive=e.ctrlKey||e.metaKey; if(e.shiftKey){ setSelectionRange(h, additive); } else if(e.target.classList.contains('row-check')){ e.target.checked?selected.add(h):selected.delete(h); lastSelectedHash=h; selectedHash=selected.size?h:null; } else { selectedHash=h; if(!additive)selected.clear(); selected.add(h); lastSelectedHash=h; loadDetails(activeTab()); } updateBulkBar(); scheduleRender(true); } const copy=e.target.closest('[data-copy]'); if(copy) copySelected(copy.dataset.copy); const torrentExport=e.target.closest('[data-download-torrent]'); if(torrentExport){ downloadTorrentFiles(); return; } const smartEx=e.target.closest('#smartExcludeCtx'); if(smartEx){ selectedHashes().forEach(h=>post('/api/smart-queue/exclusion',{hash:h,excluded:true,reason:'manual'}).catch(()=>{})); toast('Smart Queue exception saved','success'); loadSmartQueue().catch(()=>{}); } const act=e.target.closest('.torrent-action,[data-action]'); if(act&&act.dataset.action&&!act.closest('#detailTabs')&&!act.closest('.mobile-card')) runAction(act.dataset.action); });\n ";
@@ -0,0 +1 @@
export const torrentTableEventsSource = "document.addEventListener('contextmenu',e=>{ const tr=e.target.closest('tr[data-hash],.mobile-card'); if(!tr)return; e.preventDefault(); selectedHash=tr.dataset.hash; if(!selected.has(selectedHash)){selected.clear();selected.add(selectedHash);scheduleRender(true);} const m=$('ctxMenu'); m.style.left=`${e.pageX}px`; m.style.top=`${e.pageY}px`; m.style.display='block'; });\n setupDetailResizer();\n document.querySelectorAll('.torrent-table thead th[data-sort]').forEach(th=>th.addEventListener('click',()=>{ const key=th.dataset.sort; if(sortState.key===key) sortState.dir*=-1; else sortState={key,dir:1}; saveTorrentSortPreference(); scheduleRender(true); })); $('tableWrap')?.addEventListener('scroll',()=>scheduleRender(false),{passive:true}); $('selectAll')?.addEventListener('change',e=>{selected.clear(); if(e.target.checked)visibleRows.forEach(t=>selected.add(t.hash)); updateBulkBar(); scheduleRender(true);}); $('searchBox')?.addEventListener('input',()=>{if($('tableWrap'))$('tableWrap').scrollTop=0;scheduleRender(true);}); document.querySelectorAll('.filter').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.filter').forEach(x=>x.classList.remove('active')); b.classList.add('active'); activeTrackerFilter=''; activeFilter=b.dataset.filter; mobileActiveFilterKey=activeFilter; saveActiveFilterPreference(); if($('tableWrap'))$('tableWrap').scrollTop=0; scheduleRender(true);})); document.querySelectorAll('#detailTabs .nav-link').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('#detailTabs .nav-link').forEach(x=>x.classList.remove('active')); b.classList.add('active'); loadDetails(b.dataset.tab);})); document.addEventListener('change',e=>{ const sel=e.target.closest('.file-priority'); if(sel){ setFilePriorities([{index:Number(sel.dataset.index),priority:Number(sel.value)}]); return; } if(e.target && e.target.id==='fileSelectAll'){ document.querySelectorAll('#detailPane .file-check').forEach(cb=>cb.checked=e.target.checked); } }); document.addEventListener('click',e=>{ const bulk=e.target.closest('.file-priority-bulk'); if(!bulk) return; const priority=Number(bulk.dataset.priority); const checked=[...document.querySelectorAll('#detailPane .file-check:checked')].map(cb=>({index:Number(cb.dataset.index),priority})); if(!checked.length) return toast('No files selected','warning'); setFilePriorities(checked); }); document.addEventListener('click',e=>{ const tree=e.target.closest('.file-tree-refresh'); if(tree){ loadFileTree(); return; } const mediaInfo=e.target.closest('.file-media-info'); if(mediaInfo){ openMediaInfo(mediaInfo.dataset.index); return; } const oneDownload=e.target.closest('.file-download-one'); if(oneDownload){ openTemporaryDownload(`/api/torrents/${encodeURIComponent(selectedHash)}/files/${oneDownload.dataset.index}/download-link`).catch(err=>toast(err.message,'danger')); return; } const selectedDownload=e.target.closest('.file-download-selected'); if(selectedDownload){ downloadSelectedFiles(); return; } const allZip=e.target.closest('.file-download-zip'); if(allZip){ downloadZip(null); return; } const folder=e.target.closest('.folder-priority'); if(folder){ post(`/api/torrents/${encodeURIComponent(selectedHash)}/files/folder-priority`,{path:folder.dataset.path||'',priority:Number(folder.dataset.priority||0)}).then(()=>{toast('Folder priority updated','success');loadDetails('files');}).catch(err=>toast(err.message,'danger')); } }); document.addEventListener('click',e=>{ const cell=e.target.closest('.chunk-cell'); if(cell){ cell.classList.toggle('is-selected'); if(typeof updateChunkSelectionInfo==='function') updateChunkSelectionInfo(); return; } const refresh=e.target.closest('.chunk-refresh'); if(refresh){ loadDetails('chunks'); return; } const recheck=e.target.closest('.chunk-action-recheck'); if(recheck){ runChunkAction('recheck',{}); return; } const prio=e.target.closest('.chunk-action-prioritize'); if(prio){ const range=selectedChunkRange(); if(!range) return toast('No chunks selected','warning'); runChunkAction('prioritize_files',{...range,priority:2}); } }); document.addEventListener('click',e=>{ const add=e.target.closest('#trackerAddBtn'); if(add){ const url=$('trackerAddUrl')?.value||''; trackerAction('add',{url}); return; } const del=e.target.closest('.tracker-delete'); if(del && !del.disabled){ trackerAction('delete',{index:Number(del.dataset.index)}); return; } const rea=e.target.closest('#trackerReannounceBtn'); if(rea) trackerAction('reannounce',{}); }); ";
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
export const torrentTableStateSource = " function buildVisibleRows(){ visibleRows=[...torrents.values()].filter(rowVisible).sort(compareRows); $('statShown').textContent=visibleRows.length; }\n function visibleColumnKeys(){ return ['select', ...COLUMN_DEFS.map(([key])=>key)].filter(key => key === 'select' || !hiddenColumns.has(key)); }\n function applyColumnWidths(){\n // Note: Widths are applied to headers and virtualized body rows, keeping all cells aligned after live renders.\n const table = document.querySelector('.torrent-table');\n if(!table) return;\n let total = 0;\n visibleColumnKeys().forEach(key => { total += columnWidths[key] || DEFAULT_COLUMN_WIDTHS[key] || 120; });\n table.style.width = `${total}px`;\n table.style.minWidth = `${total}px`;\n document.querySelectorAll('.torrent-table [data-col]').forEach(el=>{\n const key = el.dataset.col;\n const width = columnWidths[key] || DEFAULT_COLUMN_WIDTHS[key] || 120;\n el.style.width = `${width}px`;\n el.style.minWidth = `${width}px`;\n el.style.maxWidth = `${width}px`;\n });\n }\n function applyColumnVisibility(){\n document.querySelectorAll('[data-col]').forEach(el=>el.classList.toggle('hidden-col', hiddenColumns.has(el.dataset.col)));\n applyColumnWidths();\n }\n function saveColumnWidthsPreference(){\n saveBrowserViewPrefs({columnWidths});\n savePreferencePatch({table_columns_json:columnPrefsPayload()}, 300);\n }\n function setupColumnResizers(){\n document.querySelectorAll('.torrent-table thead th[data-col]').forEach(th=>{\n const key = th.dataset.col;\n if(!key || key === 'select' || th.querySelector('.column-resize-handle')) return;\n const handle = document.createElement('span');\n handle.className = 'column-resize-handle';\n handle.title = 'Drag to resize column';\n handle.setAttribute('aria-hidden', 'true');\n th.appendChild(handle);\n let startX = 0, startWidth = 0, dragged = false;\n const onMove = (event) => {\n dragged = true;\n columnWidths[key] = clampNumber(startWidth + event.clientX - startX, COLUMN_WIDTH_MIN, COLUMN_WIDTH_MAX, startWidth);\n applyColumnWidths();\n };\n const onUp = () => {\n document.body.classList.remove('resizing-columns');\n document.removeEventListener('pointermove', onMove);\n document.removeEventListener('pointerup', onUp);\n if(dragged) saveColumnWidthsPreference();\n };\n handle.addEventListener('pointerdown', event=>{\n event.preventDefault();\n event.stopPropagation();\n dragged = false;\n startX = event.clientX;\n startWidth = columnWidths[key] || th.getBoundingClientRect().width || DEFAULT_COLUMN_WIDTHS[key] || 120;\n document.body.classList.add('resizing-columns');\n document.addEventListener('pointermove', onMove);\n document.addEventListener('pointerup', onUp);\n });\n handle.addEventListener('click', event=>event.stopPropagation());\n });\n }\n function syncActiveFilterSelection(){ syncFilterButtons(); }\n";
@@ -0,0 +1 @@
export const torrentTrackerDetailsSource = " function fmtTs(value){ const n=Number(value||0); if(!n) return '-'; try{return new Date(n*1000).toLocaleString();}catch(e){return String(n);} }\n function trackerSeedsPeers(t){ const hasScrape = t.seeds !== null || t.peers !== null; return hasScrape ? `${t.seeds ?? \"-\"} / ${t.peers ?? \"-\"}` : \"-\"; }\n function renderTrackers(trackers){\n // Note: Tracker URL editing is intentionally replaced by safe deletion; adding trackers remains unchanged.\n const pane=$('detailPane');\n const list=trackers||[];\n const canDelete=list.length>1;\n const rows=list.map(t=>{\n const idx=esc(t.index), url=esc(t.url);\n const deleteDisabled=canDelete ? '' : ' disabled title=\"At least one tracker must remain\"';\n return [`<span class=\"text-muted\">#${idx}</span>`, `<span class=\"tracker-url-text\">${url || '<span class=\"text-muted\">-</span>'}</span>`, t.enabled?'yes':'no', esc(trackerSeedsPeers(t)), esc(t.downloaded ?? '-'), fmtTs(t.last_announce), `<div class=\"tracker-actions\"><button class=\"btn btn-xs btn-outline-danger tracker-delete\" data-index=\"${idx}\"${deleteDisabled}><i class=\"fa-solid fa-trash\"></i> Delete</button></div>`];\n });\n // Note: Trackers share the responsive wrapper so long URLs do not break the details pane.\n pane.innerHTML=`<div class=\"tracker-toolbar\"><div class=\"input-group input-group-sm\"><input id=\"trackerAddUrl\" class=\"form-control tracker-add-input\" placeholder=\"https://tracker.example/announce\"><button id=\"trackerAddBtn\" class=\"btn btn-outline-primary\"><i class=\"fa-solid fa-plus\"></i> Add tracker</button></div><button id=\"trackerReannounceBtn\" class=\"btn btn-sm btn-outline-primary\"><i class=\"fa-solid fa-bullhorn\"></i> Reannounce</button></div>${responsiveTable(['#','URL','On','Seeds / Peers','Done','Last announce','Actions'], rows.length?rows:[[ '<span class=\"text-muted\">-</span>','<span class=\"text-muted\">No trackers.</span>','','','','','' ]], 'tracker-table')}`;\n }\n async function trackerAction(action,payload={}){\n if(!selectedHash) return toastMessage('toast.noTorrentSelected','warning');\n setBusy(true);\n try{\n const j=await post(`/api/torrents/${encodeURIComponent(selectedHash)}/trackers/${action}`,payload);\n toast(j.message || appMessage('toast.trackerActionDone',{action}),'success');\n await loadDetails('trackers');\n }catch(e){toast(e.message,'danger');}\n finally{setBusy(false);}\n }\n\n";
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
export const trafficHistoryDataSource = " let lastTrafficHistory = null;\n let lastTrafficHistoryRange = '7d';\n let trafficHistoryAbort = null;\n const trafficHistoryCache = new Map();\n\n async function loadTrafficHistory(range=\"7d\", force=false){\n const info=$('trafficHistoryInfo');\n const volume=$('trafficHistoryChart');\n const speed=$('trafficSpeedChart');\n if(!volume||!speed) return;\n lastTrafficHistoryRange=range;\n const cached=trafficHistoryCache.get(range);\n if(cached && !force){\n lastTrafficHistory=cached;\n drawTrafficHistory(cached);\n updateTrafficHistoryInfo(cached);\n refreshTrafficHistoryInBackground(range);\n return;\n }\n if(info) info.textContent='Loading...';\n await fetchTrafficHistory(range, true);\n }\n\n async function refreshTrafficHistoryInBackground(range){\n try{ await fetchTrafficHistory(range, false); }catch(_){ }\n }\n\n async function fetchTrafficHistory(range, showErrors){\n if(trafficHistoryAbort) trafficHistoryAbort.abort();\n trafficHistoryAbort = new AbortController();\n try{\n const res=await fetch(`/api/traffic/history?range=${encodeURIComponent(range)}`,{signal:trafficHistoryAbort.signal,cache:'no-store'});\n const j=await res.json();\n if(!j.ok) throw new Error(j.error||'Failed to load history');\n const history=j.history || {rows:[],range};\n trafficHistoryCache.set(range, history);\n if(range===lastTrafficHistoryRange){\n lastTrafficHistory=history;\n drawTrafficHistory(history);\n updateTrafficHistoryInfo(history);\n }\n }catch(e){\n if(e.name==='AbortError') return;\n if(showErrors){\n const info=$('trafficHistoryInfo');\n if(info) info.textContent=e.message;\n [$('trafficHistoryChart'),$('trafficSpeedChart')].forEach(c=>{ if(c) c.getContext('2d').clearRect(0,0,c.width,c.height); });\n }\n }finally{\n trafficHistoryAbort=null;\n }\n }\n\n function updateTrafficHistoryInfo(hist){\n const info=$('trafficHistoryInfo');\n if(!info) return;\n const rows=Array.isArray(hist.rows)?hist.rows:[];\n const bucket=hist.bucket||'bucket';\n info.textContent=rows.length ? `${rows.length} ${bucket} bucket(s), retention ${hist.retention_days||90} days.` : 'No retained samples yet. Data is stored every minute while pyTorrent is running.';\n }\n\n";