// Dashboard State let isProcessing = false; let currentConfig = {}; let currentProfiles = {}; let editingProfileName = null; // Initialize document.addEventListener('DOMContentLoaded', () => { loadConfiguration(); loadProfiles(); refreshData(); setInterval(refreshData, 5000); }); // Tab Management function switchTab(tabName) { document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); document.getElementById(tabName + '-tab').classList.add('active'); if (tabName === 'settings') loadConfiguration(); if (tabName === 'profiles') loadProfiles(); } // Data Refresh async function refreshData() { await Promise.all([ updateStats(), updateSystemStats(), updateActivity(), updateLogs(), checkProcessingStatus() ]); } // Configuration Management async function loadConfiguration() { try { const res = await fetch('/api/config'); const result = await res.json(); if (result.success) { currentConfig = result.data; document.getElementById('movies_dir').value = result.data.movies_dir || ''; document.getElementById('archive_dir').value = result.data.archive_dir || ''; document.getElementById('work_dir').value = result.data.work_dir || ''; if (result.data.parallel) { document.getElementById('max_workers').value = result.data.parallel.max_workers || 2; document.getElementById('gpu_slots').value = result.data.parallel.gpu_slots || 1; document.getElementById('cpu_slots').value = result.data.parallel.cpu_slots || 4; } if (result.data.processing) { document.getElementById('skip_without_subtitles').checked = result.data.processing.skip_without_subtitles !== false; document.getElementById('cleanup_stale_work').checked = result.data.processing.cleanup_stale_work !== false; } if (result.data.advanced) { document.getElementById('prefer_gpu').checked = result.data.advanced.prefer_gpu !== false; document.getElementById('fallback_to_cpu').checked = result.data.advanced.fallback_to_cpu !== false; } } } catch (e) { console.error('Load config failed:', e); } } async function validateConfiguration() { const config = gatherConfig(); try { const res = await fetch('/api/config/validate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) }); const result = await res.json(); if (result.success) displayValidation(result.data); } catch (e) { alert('Validation failed: ' + e.message); } } async function saveConfiguration() { const config = gatherConfig(); try { const res = await fetch('/api/config', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) }); const result = await res.json(); if (result.success) { alert('✓ Configuration saved!'); currentConfig = result.data; } else { alert('Failed: ' + result.error); } } catch (e) { alert('Save failed: ' + e.message); } } function gatherConfig() { return { movies_dir: document.getElementById('movies_dir').value, archive_dir: document.getElementById('archive_dir').value, work_dir: document.getElementById('work_dir').value, state_db: currentConfig.state_db || '/var/lib/reencode/state.db', log_dir: currentConfig.log_dir || '/var/log/reencode', parallel: { max_workers: parseInt(document.getElementById('max_workers').value), gpu_slots: parseInt(document.getElementById('gpu_slots').value), cpu_slots: parseInt(document.getElementById('cpu_slots').value) }, processing: { file_extensions: currentConfig.processing?.file_extensions || ['mkv', 'mp4', 'avi', 'm4v'], skip_without_subtitles: document.getElementById('skip_without_subtitles').checked, cleanup_stale_work: document.getElementById('cleanup_stale_work').checked }, advanced: { auto_detect_encoders: true, prefer_gpu: document.getElementById('prefer_gpu').checked, fallback_to_cpu: document.getElementById('fallback_to_cpu').checked, progress_interval: 10 }, profiles: currentConfig.profiles || {} }; } function displayValidation(data) { let html = ''; if (data.errors.length === 0 && data.warnings.length === 0) { html = '
✓ Configuration is valid!
'; } else { if (data.errors.length > 0) { html += '
Errors:
'; } if (data.warnings.length > 0) { html += '
Warnings:
'; } } document.getElementById('validationMessages').innerHTML = html; } // Profile Management async function loadProfiles() { try { const res = await fetch('/api/profiles'); const result = await res.json(); if (result.success) { currentProfiles = result.data.profiles; renderProfiles(); populateProfileSelects(result.data.default); } } catch (e) { console.error('Load profiles failed:', e); } } function renderProfiles() { let html = ''; for (const [name, profile] of Object.entries(currentProfiles)) { html += `
${name}
Encoder: ${profile.encoder} | Preset: ${profile.preset} | Quality: ${profile.quality}
`; } document.getElementById('profilesList').innerHTML = html; } function populateProfileSelects(defaultProfile) { let opts = ''; for (const name of Object.keys(currentProfiles)) { opts += ``; } document.getElementById('profileSelect').innerHTML = opts; document.getElementById('default_profile').innerHTML = opts.replace('', ''); if (defaultProfile) document.getElementById('default_profile').value = defaultProfile; } function addNewProfile() { editingProfileName = null; document.getElementById('modalTitle').textContent = 'Add Profile'; document.getElementById('modal_profile_name').value = ''; document.getElementById('modal_profile_name').disabled = false; document.getElementById('modal_encoder').value = 'nvidia_nvenc_h265'; document.getElementById('modal_preset').value = 'medium'; document.getElementById('modal_quality').value = '23'; document.getElementById('modal_audio_codec').value = 'copy'; document.getElementById('profileModal').classList.add('active'); } function editProfile(name) { editingProfileName = name; const profile = currentProfiles[name]; document.getElementById('modalTitle').textContent = 'Edit: ' + name; document.getElementById('modal_profile_name').value = name; document.getElementById('modal_profile_name').disabled = true; document.getElementById('modal_encoder').value = profile.encoder; document.getElementById('modal_preset').value = profile.preset; document.getElementById('modal_quality').value = profile.quality; document.getElementById('modal_audio_codec').value = profile.audio_codec || 'copy'; document.getElementById('profileModal').classList.add('active'); } function deleteProfile(name) { if (confirm(`Delete "${name}"?`)) { delete currentProfiles[name]; renderProfiles(); } } function closeProfileModal() { document.getElementById('profileModal').classList.remove('active'); } function saveProfileFromModal() { const name = document.getElementById('modal_profile_name').value.trim(); if (!name) return alert('Name required'); currentProfiles[name] = { encoder: document.getElementById('modal_encoder').value, preset: document.getElementById('modal_preset').value, quality: parseInt(document.getElementById('modal_quality').value), audio_codec: document.getElementById('modal_audio_codec').value }; renderProfiles(); populateProfileSelects(currentConfig.profiles?.default); closeProfileModal(); } async function saveProfiles() { const config = gatherConfig(); config.profiles = { default: document.getElementById('default_profile').value, definitions: currentProfiles }; try { const res = await fetch('/api/config', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) }); const result = await res.json(); if (result.success) { alert('✓ Profiles saved!'); currentConfig = result.data; } else { alert('Failed: ' + result.error); } } catch (e) { alert('Save failed: ' + e.message); } } // Job Control async function startProcessing() { const profile = document.getElementById('profileSelect').value; const dryRun = document.getElementById('dryRunCheckbox').checked; try { const res = await fetch('/api/jobs/start', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({profile: profile || null, dry_run: dryRun}) }); const result = await res.json(); if (result.success) { isProcessing = true; updateUIState(); setTimeout(refreshData, 1000); } else { alert('Failed: ' + result.message); } } catch (e) { alert('Error: ' + e.message); } } async function stopProcessing() { if (!confirm('Stop processing?')) return; try { const res = await fetch('/api/jobs/stop', {method: 'POST'}); const result = await res.json(); if (result.success) { isProcessing = false; updateUIState(); setTimeout(refreshData, 1000); } } catch (e) { alert('Error: ' + e.message); } } async function checkProcessingStatus() { try { const res = await fetch('/api/processing'); const result = await res.json(); if (result.success) { isProcessing = result.data.active; updateUIState(); } } catch (e) {} } function updateUIState() { document.getElementById('btnStart').disabled = isProcessing; document.getElementById('btnStop').disabled = !isProcessing; const badge = document.getElementById('statusBadge'); if (isProcessing) { badge.textContent = 'Processing'; badge.className = 'status-badge status-active'; } else { badge.textContent = 'Idle'; badge.className = 'status-badge status-idle'; } } // Stats Updates async function updateStats() { try { const res = await fetch('/api/stats'); const result = await res.json(); if (result.success) { const d = result.data; document.getElementById('statPending').textContent = d.pending || 0; document.getElementById('statProcessing').textContent = d.processing || 0; document.getElementById('statCompleted').textContent = d.completed || 0; document.getElementById('statFailed').textContent = d.failed || 0; document.getElementById('statSkipped').textContent = d.skipped || 0; const total = (d.completed || 0) + (d.failed || 0) + (d.pending || 0) + (d.processing || 0); const done = (d.completed || 0) + (d.failed || 0); const prog = total > 0 ? (done / total * 100) : 0; document.getElementById('progressBar').style.width = prog + '%'; document.getElementById('originalSize').textContent = formatBytes(d.original_size); document.getElementById('encodedSize').textContent = formatBytes(d.encoded_size); document.getElementById('spaceSaved').textContent = formatBytes(d.space_saved) + ' (' + d.space_saved_percent + '%)'; document.getElementById('avgFps').textContent = (d.avg_fps || 0).toFixed(1) + ' fps'; } } catch (e) {} } async function updateSystemStats() { try { const res = await fetch('/api/system'); const result = await res.json(); if (result.success) { let html = ''; if (result.data.gpu && result.data.gpu.length > 0) { result.data.gpu.forEach(gpu => { html += `

🎮 ${gpu.name}

GPU: ${gpu.utilization}% | Memory: ${gpu.memory_used}/${gpu.memory_total}MB | Temp: ${gpu.temperature}°C
`; }); } document.getElementById('systemStats').innerHTML = html; } } catch (e) {} } async function updateActivity() { try { const res = await fetch('/api/activity?limit=10'); const result = await res.json(); if (result.success) { let html = ''; result.data.forEach(item => { const icon = item.state === 'completed' ? '✓' : '✗'; html += `
${icon} ${item.relative_path}
`; }); document.getElementById('activityList').innerHTML = html || '

No activity

'; } } catch (e) {} } async function updateLogs() { try { const res = await fetch('/api/logs?lines=50'); const result = await res.json(); if (result.success) { const html = result.data.map(line => `
${escapeHtml(line)}
`).join(''); document.getElementById('logsContainer').innerHTML = html; } } catch (e) {} } // Utilities function formatBytes(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }