/** * Video Converter Suite - Control Panel JavaScript * Nuclear Power Plant Style UI Controller */ // ============ STATE ============ const state = { currentPage: 'dashboard', selectedFormat: 'mp4', selectedPreset: 'balanced', selectedResolution: 'original', uploadedFile: null, uploadedFilePath: null, jobs: [], streams: [], pipelines: [], activePipelineId: null, pipelineStages: [], activeStreamId: null, wsConnected: false, refreshInterval: null, }; // ============ INIT ============ document.addEventListener('DOMContentLoaded', () => { initClock(); initNavigation(); initUploadZone(); startAutoRefresh(); refreshStatus(); addLog('System initialisiert', 'info'); }); // ============ CLOCK ============ function initClock() { const el = document.getElementById('systemClock'); function update() { const now = new Date(); el.textContent = now.toTimeString().split(' ')[0]; } update(); setInterval(update, 1000); } // ============ NAVIGATION ============ function initNavigation() { document.querySelectorAll('.nav-tab').forEach(tab => { tab.addEventListener('click', () => { const page = tab.dataset.page; switchPage(page); }); }); } function switchPage(page) { state.currentPage = page; document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); document.querySelector(`.nav-tab[data-page="${page}"]`)?.classList.add('active'); document.querySelectorAll('.page-content').forEach(p => p.style.display = 'none'); document.getElementById(`page-${page}`).style.display = ''; // Refresh page-specific data if (page === 'dashboard') refreshStatus(); if (page === 'streams') refreshStreams(); if (page === 'pipelines') refreshPipelines(); if (page === 'queue') refreshQueue(); } // ============ UPLOAD ============ function initUploadZone() { const zone = document.getElementById('uploadZone'); if (!zone) return; zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', () => { zone.classList.remove('dragover'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { uploadFile(e.dataTransfer.files[0]); } }); } function handleFileSelect(event) { if (event.target.files.length > 0) { uploadFile(event.target.files[0]); } } async function uploadFile(file) { state.uploadedFile = file; addLog(`Upload gestartet: ${file.name} (${formatBytes(file.size)})`, 'info'); const formData = new FormData(); formData.append('file', file); try { const resp = await fetch('/api/upload', { method: 'POST', body: formData, }); const data = await resp.json(); if (data.error) { addLog(`Upload-Fehler: ${data.error}`, 'error'); notify('Upload fehlgeschlagen: ' + data.error, 'error'); return; } state.uploadedFilePath = data.path; displayUploadedFile(data); document.getElementById('btnStartConvert').disabled = false; addLog(`Upload abgeschlossen: ${file.name}`, 'success'); notify('Datei hochgeladen: ' + file.name, 'success'); } catch (err) { addLog(`Upload-Fehler: ${err.message}`, 'error'); notify('Upload fehlgeschlagen', 'error'); } } function displayUploadedFile(data) { const el = document.getElementById('uploadedFileInfo'); el.style.display = 'block'; const info = data.info || {}; const video = info.video || {}; const audio = info.audio || {}; el.innerHTML = `
${data.original_name} ${formatBytes(data.size)}
Format: ${info.format_name || 'N/A'}
Dauer: ${formatDuration(info.duration || 0)}
${video ? `
Video: ${video.codec || 'N/A'} ${video.width || ''}x${video.height || ''}
FPS: ${video.fps || 'N/A'}
` : ''} ${audio ? `
Audio: ${audio.codec || 'N/A'}
Sample: ${audio.sample_rate || 'N/A'} Hz
` : ''}
`; } // ============ FORMAT SELECTION ============ function selectFormat(format) { state.selectedFormat = format; document.querySelectorAll('.format-switch').forEach(s => s.classList.remove('selected')); document.querySelectorAll(`.format-switch[data-format="${format}"]`).forEach(s => s.classList.add('selected')); addLog(`Format gewählt: ${format.toUpperCase()}`, 'info'); } function selectPreset(preset) { state.selectedPreset = preset; document.querySelectorAll('#presetPanel .switch-unit').forEach(s => s.classList.remove('active')); document.querySelector(`#presetPanel .switch-unit[data-preset="${preset}"]`)?.classList.add('active'); } function selectResolution(res) { state.selectedResolution = res; document.querySelectorAll('#resolutionPanel .switch-unit').forEach(s => s.classList.remove('active')); document.querySelector(`#resolutionPanel .switch-unit[data-resolution="${res}"]`)?.classList.add('active'); } // ============ CONVERSION ============ async function startConversion() { if (!state.uploadedFilePath) { notify('Keine Datei hochgeladen', 'warning'); return; } const params = { input_file: state.uploadedFilePath, output_format: state.selectedFormat, preset: state.selectedPreset, }; if (state.selectedResolution !== 'original') { params.resolution = state.selectedResolution; } addLog(`Konvertierung gestartet: ${state.selectedFormat.toUpperCase()} / ${state.selectedPreset}`, 'info'); document.getElementById('conversionStatus').textContent = 'Konvertierung wird gestartet...'; try { const resp = await fetch('/api/convert', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }); const data = await resp.json(); if (data.error) { addLog(`Fehler: ${data.error}`, 'error'); notify(data.error, 'error'); return; } addLog(`Job erstellt: ${data.id}`, 'success'); notify('Konvertierung gestartet', 'success'); document.getElementById('btnStopAll').style.display = ''; document.getElementById('conversionStatus').textContent = `Job ${data.id} läuft...`; startJobPolling(data.id); } catch (err) { addLog(`Fehler: ${err.message}`, 'error'); notify('Konvertierung fehlgeschlagen', 'error'); } } function startJobPolling(jobId) { const poll = setInterval(async () => { try { const resp = await fetch(`/api/jobs/${jobId}/progress`); const progress = await resp.json(); document.getElementById('conversionStatus').textContent = `${progress.percent || 0}% | FPS: ${progress.fps || 0} | Speed: ${progress.speed || '0x'} | Zeit: ${progress.time || '00:00:00'}`; updateJobInList(jobId, progress); if (progress.percent >= 100) { clearInterval(poll); addLog(`Job ${jobId} abgeschlossen`, 'success'); notify('Konvertierung abgeschlossen!', 'success'); document.getElementById('conversionStatus').textContent = 'Konvertierung abgeschlossen!'; refreshJobs(); } } catch (e) { // Keep polling } }, 1000); } async function stopAllJobs() { if (!confirm('Alle laufenden Jobs stoppen?')) return; try { const resp = await fetch('/api/jobs'); const data = await resp.json(); for (const job of (data.jobs || [])) { if (job.status === 'running') { await fetch(`/api/jobs/${job.id}/cancel`, { method: 'POST' }); addLog(`Job ${job.id} gestoppt`, 'warn'); } } notify('Alle Jobs gestoppt', 'warning'); document.getElementById('btnStopAll').style.display = 'none'; refreshJobs(); } catch (e) { notify('Fehler beim Stoppen', 'error'); } } // ============ JOBS ============ async function refreshJobs() { try { const resp = await fetch('/api/jobs'); const data = await resp.json(); state.jobs = data.jobs || []; renderJobList(); } catch (e) { // Silently fail } } function renderJobList() { const el = document.getElementById('jobList'); if (!state.jobs.length) { el.innerHTML = '
Keine aktiven Jobs
'; return; } el.innerHTML = state.jobs.map(job => `
${job.thumbnail ? `` : '🎦'}
${job.input_file ? job.input_file.split('/').pop() : 'Unknown'}
${job.output_format?.toUpperCase() || ''} | ${job.preset || ''} | ${job.resolution || 'Original'}
${job.status === 'completed' ? '100%' : '0%'}
${job.status}
${job.status === 'running' ? `` : ''} ${job.status === 'completed' ? `` : ''}
`).join(''); // Update active job count const running = state.jobs.filter(j => j.status === 'running').length; document.getElementById('activeJobCount').textContent = running; document.getElementById('gaugeJobs').textContent = running; } function updateJobInList(jobId, progress) { const bar = document.getElementById(`progress-${jobId}`); const text = document.getElementById(`progress-text-${jobId}`); const speed = document.getElementById(`progress-speed-${jobId}`); if (bar) bar.style.width = `${progress.percent || 0}%`; if (text) text.textContent = `${progress.percent || 0}%`; if (speed) speed.textContent = `${progress.fps || 0} fps | ${progress.speed || ''}`; } async function cancelJob(id) { await fetch(`/api/jobs/${id}/cancel`, { method: 'POST' }); addLog(`Job ${id} abgebrochen`, 'warn'); refreshJobs(); } async function deleteJob(id) { await fetch(`/api/jobs/${id}`, { method: 'DELETE' }); addLog(`Job ${id} gelöscht`, 'info'); refreshJobs(); } function downloadJob(id) { window.open(`/api/download/${id}`, '_blank'); } // ============ STREAMS ============ async function refreshStreams() { try { const resp = await fetch('/api/streams'); const data = await resp.json(); state.streams = data.streams || []; renderStreamMatrix(); updateStreamSelect(); } catch (e) {} } function renderStreamMatrix() { const el = document.getElementById('streamMatrix'); if (!state.streams.length) { el.innerHTML = '
Keine aktiven Streams
'; return; } el.innerHTML = state.streams.map(s => `
${s.status === 'running' ? '▶ LIVE' : 'NO SIGNAL'} ${s.status === 'running' ? 'LIVE' : ''}
${s.input_url || 'Stream'}
${s.output_format?.toUpperCase() || ''} | ${s.resolution || 'Original'} | ${s.preset || 'fast'}
${s.status}
${s.status === 'running' ? `` : `` }
`).join(''); } function updateStreamSelect() { const sel = document.getElementById('activeStreamSelect'); const runningStreams = state.streams.filter(s => s.status === 'running'); sel.innerHTML = '' + runningStreams.map(s => `` ).join(''); } function openStreamModal() { document.getElementById('streamModal').classList.add('visible'); } function closeStreamModal() { document.getElementById('streamModal').classList.remove('visible'); } async function startNewStream() { const inputUrl = document.getElementById('streamInputUrl').value; if (!inputUrl) { notify('Bitte Stream-URL eingeben', 'warning'); return; } const params = { input_url: inputUrl, output_format: document.getElementById('streamOutputFormat').value, resolution: document.getElementById('streamResolution').value || null, preset: document.getElementById('streamPreset').value, }; try { const resp = await fetch('/api/streams', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }); const data = await resp.json(); if (data.error) { notify(data.error, 'error'); return; } addLog(`Stream gestartet: ${data.id}`, 'success'); notify('Stream gestartet', 'success'); closeStreamModal(); refreshStreams(); } catch (e) { notify('Stream-Start fehlgeschlagen', 'error'); } } async function stopStream(id) { await fetch(`/api/streams/${id}`, { method: 'DELETE' }); addLog(`Stream ${id} gestoppt`, 'warn'); refreshStreams(); } async function deleteStream(id) { await fetch(`/api/streams/${id}`, { method: 'DELETE' }); refreshStreams(); } function selectActiveStream(id) { state.activeStreamId = id; // Highlight current format const stream = state.streams.find(s => s.id === id); document.querySelectorAll('[data-stream-format]').forEach(el => el.classList.remove('selected')); if (stream) { document.querySelector(`[data-stream-format="${stream.output_format}"]`)?.classList.add('selected'); } } async function switchStreamFormat(format) { if (!state.activeStreamId) { notify('Bitte zuerst einen Stream wählen', 'warning'); return; } addLog(`Format-Wechsel: ${format.toUpperCase()} für Stream ${state.activeStreamId}`, 'warn'); try { const resp = await fetch(`/api/streams/${state.activeStreamId}/switch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ format }), }); const data = await resp.json(); if (data.error) { notify(data.error, 'error'); return; } addLog(`Format gewechselt zu ${format.toUpperCase()}`, 'success'); notify(`Format umgeschaltet: ${format.toUpperCase()}`, 'success'); // Update active stream ID to new stream state.activeStreamId = data.id; refreshStreams(); // Highlight new format document.querySelectorAll('[data-stream-format]').forEach(el => el.classList.remove('selected')); document.querySelector(`[data-stream-format="${format}"]`)?.classList.add('selected'); } catch (e) { notify('Format-Wechsel fehlgeschlagen', 'error'); } } // ============ PIPELINES ============ async function refreshPipelines() { try { const resp = await fetch('/api/pipelines'); const data = await resp.json(); state.pipelines = data.pipelines || []; renderPipelineList(); } catch (e) {} } function renderPipelineList() { const el = document.getElementById('pipelineList'); if (!state.pipelines.length) { el.innerHTML = '
Keine Pipelines vorhanden
'; return; } el.innerHTML = state.pipelines.map(p => `
${p.name}
${(p.stages || []).length} Stufen | Status: ${p.status}
${p.status}
`).join(''); } async function createPipeline() { const name = prompt('Pipeline-Name:', 'Neue Pipeline'); if (!name) return; try { const resp = await fetch('/api/pipelines', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); const data = await resp.json(); addLog(`Pipeline erstellt: ${data.name}`, 'success'); refreshPipelines(); editPipeline(data.id); } catch (e) { notify('Pipeline-Erstellung fehlgeschlagen', 'error'); } } function editPipeline(id) { state.activePipelineId = id; const pipeline = state.pipelines.find(p => p.id === id); if (!pipeline) return; state.pipelineStages = pipeline.stages || []; document.getElementById('pipelineEditor').style.display = ''; renderPipelineFlow(); } function renderPipelineFlow() { const flow = document.getElementById('pipelineFlow'); let html = `
Input
Source
`; state.pipelineStages.forEach((stage, i) => { html += `
`; html += `
${stage.type}
${stage.label || stage.type}
`; }); html += `
`; html += `
Output
Target
`; flow.innerHTML = html; } function toggleStage(index) { if (state.pipelineStages[index]) { state.pipelineStages[index].enabled = !state.pipelineStages[index].enabled; renderPipelineFlow(); savePipelineStages(); } } async function addPipelineStage(type) { if (!state.activePipelineId) { notify('Bitte zuerst eine Pipeline auswählen oder erstellen', 'warning'); return; } const stageDefaults = { transcode: { params: { video_codec: 'libx264', preset: 'medium', crf: 23 } }, scale: { params: { width: 1920, height: 1080 } }, filter: { params: { brightness: 0, contrast: 1, saturation: 1 } }, audio: { params: { codec: 'aac', bitrate: '128k', sample_rate: 44100 } }, bitrate: { params: { video: '2M', audio: '128k' } }, framerate: { params: { fps: 30 } }, trim: { params: { start: '00:00:00', duration: '' } }, deinterlace: { params: {} }, denoise: { params: {} }, stabilize: { params: {} }, }; const defaults = stageDefaults[type] || { params: {} }; const stage = { type, label: type.charAt(0).toUpperCase() + type.slice(1), params: defaults.params, enabled: true, }; state.pipelineStages.push(stage); renderPipelineFlow(); await savePipelineStages(); addLog(`Stufe hinzugefügt: ${type}`, 'info'); } async function savePipelineStages() { if (!state.activePipelineId) return; try { await fetch(`/api/pipelines/${state.activePipelineId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stages: state.pipelineStages }), }); } catch (e) { console.error('Failed to save pipeline stages'); } } async function deletePipeline(id) { if (!confirm('Pipeline löschen?')) return; await fetch(`/api/pipelines/${id}`, { method: 'DELETE' }); if (state.activePipelineId === id) { state.activePipelineId = null; document.getElementById('pipelineEditor').style.display = 'none'; } refreshPipelines(); } async function runPipeline(id) { if (!state.uploadedFilePath) { notify('Bitte zuerst eine Datei hochladen (Konverter-Seite)', 'warning'); return; } try { const resp = await fetch(`/api/pipelines/${id}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input_file: state.uploadedFilePath, output_format: state.selectedFormat, }), }); const data = await resp.json(); if (data.error) { notify(data.error, 'error'); return; } addLog(`Pipeline ${id} ausgeführt, Job: ${data.id}`, 'success'); notify('Pipeline gestartet', 'success'); startJobPolling(data.id); } catch (e) { notify('Pipeline-Start fehlgeschlagen', 'error'); } } // ============ QUEUE ============ async function refreshQueue() { try { const resp = await fetch('/api/queue'); const data = await resp.json(); renderQueue(data.queue || [], data.stats || {}); } catch (e) {} } function renderQueue(queue, stats) { const el = document.getElementById('queueList'); if (!queue.length) { el.innerHTML = '
Warteschlange ist leer
'; } else { el.innerHTML = queue.map(job => `
📄
${job.input_file || job.queue_id}
Priorität: ${job.priority || 5} | ${job.output_format || 'mp4'}
${job.queue_status}
`).join(''); } document.getElementById('queueWaiting').textContent = stats.waiting || 0; document.getElementById('queueProcessing').textContent = stats.processing || 0; document.getElementById('queueCompleted').textContent = stats.completed || 0; document.getElementById('queueFailed').textContent = stats.failed || 0; } async function clearQueue() { await fetch('/api/queue', { method: 'DELETE' }); refreshQueue(); notify('Queue geleert', 'info'); } // ============ STATUS / SYSTEM ============ async function refreshStatus() { try { const resp = await fetch('/api/system'); const data = await resp.json(); updateGauges(data); refreshJobs(); } catch (e) { document.getElementById('systemStatusDot').className = 'status-dot error'; document.getElementById('systemStatusText').textContent = 'OFFLINE'; } } function updateGauges(data) { // CPU const cpuLoad = data.cpu_load?.[0] || 0; const cpuPercent = Math.min(100, cpuLoad * 25); // Normalize to ~100% at load 4 document.getElementById('gaugeCpu').textContent = cpuLoad.toFixed(1); const cpuBar = document.getElementById('gaugeCpuBar'); cpuBar.style.width = cpuPercent + '%'; cpuBar.className = 'gauge-bar-fill' + (cpuPercent > 80 ? ' danger' : cpuPercent > 50 ? ' warning' : ''); // Memory const mem = data.memory || {}; const memPercent = mem.peak ? Math.round((mem.used / mem.peak) * 100) : 0; document.getElementById('gaugeMem').textContent = memPercent; const memBar = document.getElementById('gaugeMemBar'); memBar.style.width = memPercent + '%'; memBar.className = 'gauge-bar-fill' + (memPercent > 80 ? ' danger' : memPercent > 50 ? ' warning' : ''); // Disk const diskFree = (data.disk?.free || 0) / (1024 * 1024 * 1024); const diskTotal = (data.disk?.total || 1) / (1024 * 1024 * 1024); const diskUsedPercent = Math.round(((diskTotal - diskFree) / diskTotal) * 100); document.getElementById('gaugeDisk').textContent = diskFree.toFixed(1); const diskBar = document.getElementById('gaugeDiskBar'); diskBar.style.width = diskUsedPercent + '%'; diskBar.className = 'gauge-bar-fill' + (diskUsedPercent > 90 ? ' danger' : diskUsedPercent > 70 ? ' warning' : ''); // Status document.getElementById('systemStatusDot').className = 'status-dot'; document.getElementById('systemStatusText').textContent = data.ffmpeg_available ? 'SYSTEM ONLINE' : 'FFMPEG MISSING'; if (!data.ffmpeg_available) { document.getElementById('systemStatusDot').className = 'status-dot warning'; } } function startAutoRefresh() { if (state.refreshInterval) clearInterval(state.refreshInterval); state.refreshInterval = setInterval(() => { if (state.currentPage === 'dashboard') refreshStatus(); if (state.currentPage === 'streams') refreshStreams(); }, 5000); } // ============ LOGGING ============ function addLog(message, level = 'info') { const console = document.getElementById('logConsole'); const time = new Date().toLocaleTimeString(); const line = document.createElement('div'); line.className = `log-line ${level}`; line.innerHTML = `[${time}] ${escapeHtml(message)}`; console.appendChild(line); console.scrollTop = console.scrollHeight; // Keep max 100 lines while (console.children.length > 100) { console.removeChild(console.firstChild); } } function clearLog() { document.getElementById('logConsole').innerHTML = '
[CLEAR] Log bereinigt
'; } // ============ NOTIFICATIONS ============ function notify(message, type = 'info') { const el = document.getElementById('notification'); el.className = `notification ${type} show`; el.textContent = message; setTimeout(() => { el.classList.remove('show'); }, 3000); } // ============ HELPERS ============ function formatBytes(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0, size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return size.toFixed(1) + ' ' + units[i]; } function formatDuration(seconds) { if (!seconds) return '0:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }