(function() { var dot = document.getElementById('sse-dot'); var es; function connect() { es = new EventSource('/events'); es.addEventListener('hello', function() { dot.className = 'led led-green'; }); es.addEventListener('host.state_changed', function() { window.location.reload(); }); es.addEventListener('activity.logged', function(e) { var data; try { data = JSON.parse(e.data); } catch(_) { return; } var logDiv = document.getElementById('activity-log'); if (!logDiv) return; var hostId = logDiv.getAttribute('data-host-id'); var opId = logDiv.getAttribute('data-operation-id'); if (opId) { if (String(data.operation_id) !== opId) return; } else if (hostId) { if (String(data.host_id) !== hostId) return; } else { return; } var empty = logDiv.querySelector('.empty'); if (empty) empty.remove(); var entry = document.createElement('div'); entry.className = 'log-entry log-' + data.level; var t = new Date(data.created_at); var ts = t.getHours().toString().padStart(2,'0') + ':' + t.getMinutes().toString().padStart(2,'0'); entry.innerHTML = '' + ts + '' + '' + data.source + '' + '' + data.message + ''; logDiv.insertBefore(entry, logDiv.firstChild); }); es.onerror = function() { dot.className = 'led led-red'; es.close(); setTimeout(connect, 3000); }; } connect(); })(); (function() { var form = document.getElementById('upload-form'); if (!form) return; var uploadId = 'upload-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); document.getElementById('upload-id').value = uploadId; var progressDiv = document.getElementById('upload-progress'); var progressFill = document.getElementById('progress-fill'); var progressTitle = document.getElementById('progress-title'); var progressText = document.getElementById('progress-text'); var progressDetail = document.getElementById('progress-detail'); var uploadES = new EventSource('/events'); uploadES.addEventListener('image.upload_progress', function(e) { var data; try { data = JSON.parse(e.data); } catch(_) { return; } if (data.upload_id !== uploadId) return; progressText.textContent = data.detail; if (data.stage === 'parsing' || data.stage === 'extracting') { progressTitle.textContent = 'Extracting boot files...'; progressDetail.textContent = 'Processing ISO on server...'; } if (data.stage === 'complete') { progressFill.classList.add('complete'); progressTitle.textContent = 'Complete'; progressDetail.textContent = 'Redirecting...'; } }); form.addEventListener('submit', function(e) { e.preventDefault(); if (!form.checkValidity()) { form.reportValidity(); return; } form.style.display = 'none'; progressDiv.style.display = 'block'; var formData = new FormData(form); var xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', function(ev) { if (ev.lengthComputable) { var pct = Math.round((ev.loaded / ev.total) * 100); progressFill.style.width = pct + '%'; var mb = (ev.loaded / (1024 * 1024)).toFixed(1); var totalMb = (ev.total / (1024 * 1024)).toFixed(1); progressText.textContent = 'Uploading: ' + mb + ' / ' + totalMb + ' MB (' + pct + '%)'; } }); xhr.upload.addEventListener('load', function() { progressTitle.textContent = 'Processing ISO...'; progressText.textContent = 'Upload complete. Extracting kernel and initrd...'; progressDetail.textContent = 'This may take a minute...'; }); xhr.addEventListener('load', function() { uploadES.close(); var resp; try { resp = JSON.parse(xhr.responseText); } catch(_) { resp = {}; } if (xhr.status >= 200 && xhr.status < 300 && resp.ok) { window.location.href = '/images'; } else { progressDiv.style.display = 'none'; form.style.display = 'block'; showUploadError(resp.error || 'Upload failed'); } }); xhr.addEventListener('error', function() { uploadES.close(); progressDiv.style.display = 'none'; form.style.display = 'block'; showUploadError('Network error during upload'); }); xhr.open('POST', '/images/upload'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(formData); }); function showUploadError(msg) { var errDiv = form.parentNode.querySelector('.error'); if (!errDiv) { errDiv = document.createElement('div'); errDiv.className = 'error'; form.parentNode.insertBefore(errDiv, form); } errDiv.textContent = msg; } })();