Add upload progress bar with SSE extraction status
ISO uploads now show a progress bar during file transfer (via XHR upload.onprogress) and real-time extraction status (via SSE events through the existing Hub). Falls back to plain form POST if JS is disabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+102
-7
@@ -1,18 +1,16 @@
|
||||
(function() {
|
||||
const dot = document.getElementById('sse-dot');
|
||||
let es;
|
||||
var dot = document.getElementById('sse-dot');
|
||||
var es;
|
||||
|
||||
function connect() {
|
||||
es = new EventSource('/events');
|
||||
es.addEventListener('hello', () => {
|
||||
es.addEventListener('hello', function() {
|
||||
dot.classList.remove('disconnected');
|
||||
});
|
||||
es.addEventListener('host.state_changed', (e) => {
|
||||
// Reload the page to reflect state changes
|
||||
// Future: HTMX swap individual tiles
|
||||
es.addEventListener('host.state_changed', function() {
|
||||
window.location.reload();
|
||||
});
|
||||
es.onerror = () => {
|
||||
es.onerror = function() {
|
||||
dot.classList.add('disconnected');
|
||||
es.close();
|
||||
setTimeout(connect, 3000);
|
||||
@@ -21,3 +19,100 @@
|
||||
|
||||
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;
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user