/**
* 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 = `
`;
state.pipelineStages.forEach((stage, i) => {
html += ``;
html += `
${stage.type}
${stage.label || stage.type}
`;
});
html += ``;
html += `
`;
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;
}