vm-bench/frontend/templates/polling.html

125 lines
4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Converting VM — VM Bench</title>
<link rel="stylesheet" href="/static/proxmox.css">
</head>
<body>
<header>
<div class="logo">VM Bench</div>
<div class="sub">Proxmox Image Conversion</div>
</header>
<div class="container">
<div class="panel poll-panel">
<h2>Converting VM {{ vmid }}</h2>
<p class="dim">Job: <code>{{ job_id }}</code> &nbsp;|&nbsp; {{ vm_name }}</p>
<div id="status-badge" class="badge badge-queued">
<span class="spinner"></span> Queued
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width:0%">0%</div>
</div>
<p id="status-message" class="dim">Waiting for backend...</p>
<p id="elapsed" class="dim">Elapsed: 0s</p>
<div id="error-block" class="error hidden"></div>
</div>
<!-- Reuse section (shown after completion) -->
<div id="reuse-section" class="panel hidden">
<h2>Conversion Complete</h2>
<p>Use the same source image to create another VM?</p>
<div class="btn-row">
<button onclick="reuseImage(true)" class="btn">Yes — Create Another VM</button>
<button onclick="reuseImage(false)" class="btn btn-secondary">No — Clean Up &amp; Finish</button>
</div>
</div>
</div>
<footer>
<span>vm-bench frontend</span>
<span>Job: {{ job_id }}</span>
</footer>
<script>
const jobId = "{{ job_id }}";
const vmid = "{{ vmid }}";
const startTime = Date.now();
let completed = false;
async function poll() {
try {
const resp = await fetch(`/session/status/${jobId}`);
const data = await resp.json();
const pct = data.progress_percentage || 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-fill').textContent = pct + '%';
const elapsed = Math.round((Date.now() - startTime) / 1000);
document.getElementById('elapsed').textContent = `Elapsed: ${elapsed}s`;
document.getElementById('status-message').textContent = data.message || '';
const badge = document.getElementById('status-badge');
const map = {
'queued': ['badge-queued', 'Queued'],
'processing_conversion': ['badge-running', 'Converting...'],
'importing_storage': ['badge-running', 'Importing...'],
'completed': ['badge-completed', 'Completed'],
'failed': ['badge-failed', 'Failed'],
};
const [cls, text] = map[data.status] || ['badge-queued', data.status];
badge.className = 'badge ' + cls;
badge.innerHTML = (data.status === 'processing_conversion' || data.status === 'importing_storage')
? '<span class="spinner"></span> ' + text : text;
if (data.error_details) {
document.getElementById('error-block').classList.remove('hidden');
document.getElementById('error-block').textContent = data.error_details;
}
if (data.status === 'completed' || data.status === 'failed') {
completed = true;
if (data.status === 'completed') {
document.getElementById('reuse-section').classList.remove('hidden');
document.getElementById('reuse-section').scrollIntoView({ behavior: 'smooth' });
}
return;
}
if (!completed) setTimeout(poll, 2000);
} catch (err) {
document.getElementById('error-block').classList.remove('hidden');
document.getElementById('error-block').textContent = 'Polling error: ' + err.message;
if (!completed) setTimeout(poll, 5000);
}
}
async function reuseImage(keep) {
try {
await fetch(`/session/cleanup/${jobId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ delete_staging_files: !keep }),
});
if (keep) {
window.location.href = `/?vmid=${vmid}&source={{ source_filename }}`;
} else {
window.location.href = '/';
}
} catch (err) {
alert('Cleanup failed: ' + err.message);
}
}
// Wait 3 seconds before first poll (backend needs time to queue)
setTimeout(poll, 3000);
</script>
</body>
</html>