a6603b463f
Hosts stuck in states like pxe_ready had zero visibility into why. This adds a persistent activity log that records every meaningful step (state transitions, PXE events, cluster join stages, failures) and surfaces it on the host detail page with live SSE updates. Includes a stuck-detection warning banner when a host sits in pxe_ready for >10 minutes with no iPXE request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.6 KiB
JavaScript
137 lines
4.6 KiB
JavaScript
(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');
|
|
if (String(data.host_id) !== hostId) 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 = '<span class="log-time">' + ts + '</span>' +
|
|
'<span class="log-source">' + data.source + '</span>' +
|
|
'<span class="log-msg">' + data.message + '</span>';
|
|
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;
|
|
}
|
|
})();
|