Add upload progress bar with SSE extraction status
build-and-push / test (push) Successful in 40s
build-and-push / build-and-push (push) Successful in 1m8s

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:
2026-05-10 19:16:19 -04:00
parent 4774600040
commit 443a3db9e1
6 changed files with 200 additions and 22 deletions
+102 -7
View File
@@ -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;
}
})();