2 lines
6.8 KiB
JavaScript
2 lines
6.8 KiB
JavaScript
export const operationLogsSource = " let operationLogsPage = 0;\n const operationLogsLimit = 200;\n\n function operationLogBadge(type, severity){\n const cls = severity === 'danger' ? 'danger' : severity === 'warning' ? 'warning' : type === 'torrent_completed' ? 'success' : type === 'torrent_removed' ? 'secondary' : 'info';\n return `<span class=\"badge text-bg-${cls}\">${esc(type || 'log')}</span>`;\n }\n\n function renderOperationLogStats(stats={}){\n const card = (label, value) => `<div class=\"operation-log-stat\"><b>${esc(label)}</b><span>${esc(value ?? 0)}</span></div>`;\n const types = (stats.by_type || []).map(x => card(x.event_type || 'unknown', x.n)).join('');\n const daily = (stats.by_day || []).map(x => `<div class=\"operation-log-row\"><span>${esc(x.bucket)}</span><b>${esc(x.n)}</b></div>`).join('');\n const monthly = (stats.by_month || []).map(x => `<div class=\"operation-log-row\"><span>${esc(x.bucket)}</span><b>${esc(x.n)}</b></div>`).join('');\n const actions = (stats.top_actions || []).map(x => `<div class=\"operation-log-row\"><span>${esc(x.action)}</span><b>${esc(x.n)}</b></div>`).join('');\n return `<div class=\"operation-log-stats-grid\">${card('Total logs', stats.total || 0)}${types}</div><div class=\"operation-log-panels\"><section><h6>Daily count</h6>${daily || '<span class=\"empty-mini\">No data.</span>'}</section><section><h6>Monthly count</h6>${monthly || '<span class=\"empty-mini\">No data.</span>'}</section><section><h6>Top actions</h6>${actions || '<span class=\"empty-mini\">No data.</span>'}</section></div>`;\n }\n\n function fillOperationLogSettings(settings={}){\n if($('operationLogRetentionMode')) $('operationLogRetentionMode').value = settings.retention_mode || 'days';\n if($('operationLogRetentionDays')) $('operationLogRetentionDays').value = settings.retention_days || 30;\n if($('operationLogRetentionLines')) $('operationLogRetentionLines').value = settings.retention_lines || 5000;\n }\n\n function operationLogQuery(){\n const params = new URLSearchParams();\n params.set('limit', String(operationLogsLimit));\n params.set('offset', String(operationLogsPage * operationLogsLimit));\n const type = $('operationLogTypeFilter')?.value || '';\n const q = $('operationLogSearch')?.value || '';\n if(type) params.set('type', type);\n if(q) params.set('q', q);\n return params.toString();\n }\n\n function renderOperationLogs(data={}){\n const box = $('operationLogsTable');\n if(!box) return;\n const rows = data.logs || [];\n const total = Number(data.total || 0);\n const types = ['','torrent_added','torrent_removed','torrent_completed','job_started','job_done','job_failed'];\n if($('operationLogTypeFilter') && !$('operationLogTypeFilter').dataset.ready){\n $('operationLogTypeFilter').innerHTML = types.map(t => `<option value=\"${esc(t)}\">${t ? esc(t) : 'All types'}</option>`).join('');\n $('operationLogTypeFilter').dataset.ready = '1';\n }\n box.innerHTML = responsiveTable(['Time','Type','Source','Action','Torrent','Message','Details'], rows.map(r => [\n humanDateCell(r.created_at),\n operationLogBadge(r.event_type, r.severity),\n esc(r.source || '-'),\n esc(r.action || '-'),\n compactCell(r.torrent_name || r.torrent_hash || '-', 180),\n compactCell(r.message || '', 260),\n compactCell(r.details_h || '', 220),\n ]), 'operation-log-table');\n if(!rows.length) box.innerHTML = '<div class=\"empty-state\"><i class=\"fa-solid fa-book\"></i><b>No logs.</b><span>No entries match current filters.</span></div>';\n const pages = Math.max(1, Math.ceil(total / operationLogsLimit));\n if($('operationLogsPager')) $('operationLogsPager').innerHTML = `<button id=\"operationLogsPrev\" class=\"btn btn-sm btn-outline-secondary\" ${operationLogsPage <= 0 ? 'disabled' : ''}>Prev</button><span class=\"small text-muted\">Page ${operationLogsPage + 1} / ${pages} \u00b7 ${total} logs</span><button id=\"operationLogsNext\" class=\"btn btn-sm btn-outline-secondary\" ${operationLogsPage >= pages - 1 ? 'disabled' : ''}>Next</button>`;\n $('operationLogsPrev')?.addEventListener('click', () => { operationLogsPage = Math.max(0, operationLogsPage - 1); loadOperationLogs(); });\n $('operationLogsNext')?.addEventListener('click', () => { operationLogsPage += 1; loadOperationLogs(); });\n if($('operationLogStats')) $('operationLogStats').innerHTML = renderOperationLogStats(data.stats || {});\n fillOperationLogSettings(data.settings || data.stats?.settings || {});\n }\n\n async function loadOperationLogs(reset=false){\n const box = $('operationLogsTable');\n if(!box) return;\n if(reset) operationLogsPage = 0;\n box.innerHTML = '<span class=\"spinner-border spinner-border-sm\"></span> Loading logs...';\n try{\n const data = await fetch(`/api/operation-logs?${operationLogQuery()}`).then(r => r.json());\n if(!data.ok) throw new Error(data.error || 'Cannot load logs');\n renderOperationLogs(data);\n }catch(e){\n box.innerHTML = `<div class=\"alert alert-danger mb-0\">${esc(e.message)}</div>`;\n }\n }\n\n async function saveOperationLogSettings(){\n try{\n const data = await post('/api/operation-logs/settings', {\n retention_mode: $('operationLogRetentionMode')?.value || 'days',\n retention_days: Number($('operationLogRetentionDays')?.value || 30),\n retention_lines: Number($('operationLogRetentionLines')?.value || 5000),\n });\n fillOperationLogSettings(data.settings || {});\n toast(`Log retention saved. Deleted ${data.retention?.deleted || 0} old entries.`, 'success');\n loadOperationLogs(true);\n }catch(e){ toast(e.message, 'danger'); }\n }\n\n function bindOperationLogEvents(){\n $('logsModal')?.addEventListener('show.bs.modal', () => loadOperationLogs(true));\n $('refreshOperationLogsBtn')?.addEventListener('click', () => loadOperationLogs(true));\n $('operationLogTypeFilter')?.addEventListener('change', () => loadOperationLogs(true));\n $('operationLogSearch')?.addEventListener('input', debounce(() => loadOperationLogs(true), 300));\n $('saveOperationLogRetentionBtn')?.addEventListener('click', saveOperationLogSettings);\n $('applyOperationLogRetentionBtn')?.addEventListener('click', async () => { try{ const j = await post('/api/operation-logs/apply-retention', {}); toast(`Deleted ${j.deleted || 0} old log entries.`, 'success'); loadOperationLogs(true); }catch(e){ toast(e.message, 'danger'); } });\n $('clearOperationLogsBtn')?.addEventListener('click', async () => { if(!confirm('Clear operation logs for this profile?')) return; try{ const j = await post('/api/operation-logs/clear', {event_type: $('operationLogTypeFilter')?.value || ''}); toast(`Deleted ${j.deleted || 0} log entries.`, 'success'); loadOperationLogs(true); }catch(e){ toast(e.message, 'danger'); } });\n }\n";
|