vm-bench/frontend/templates/index.html
Claus Lohmar 778e929985 refactor: remove browser file upload — download + SCP only
- Removed UploadFile handling, upload-raw endpoint, source type selector
- index.html: single URL input, 'Download & Analyse' button
- session_upload simplified to download-only (aria2c)
- Removed dead code: _check_disk_space, UPLOAD_PROGRESS_INTERVAL,
  MIN_FREE_DISK_GB, UploadFile/File imports
- SCP Pull remains as separate /scp page
2026-07-23 12:33:23 +00:00

280 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block content %}
<div class="pve-panel" id="step1">
<div class="pve-panel-header">
<span class="pve-panel-title">New Conversion Session</span>
</div>
<div class="pve-panel-body">
<form id="session-form" onsubmit="startSession(event)">
<div class="pve-form-group">
<label for="vm-name">VM Name *</label>
<input type="text" id="vm-name" name="vm_name" required class="pve-input"
placeholder="e.g. debian-test" value="{{ prefill_vmname or '' }}">
<span class="pve-hint">Display name for the VM on the Proxmox host.</span>
</div>
<div class="pve-form-group">
<label for="vmid">VM ID *</label>
<input type="number" id="vmid" name="vmid" required class="pve-input"
placeholder="e.g. 21050" min="21000" max="21100">
<span class="pve-hint">Range 2100021100. Must be unique on the Proxmox host.</span>
</div>
<div class="pve-form-group">
<label for="source-url">Download URL *</label>
<input type="url" id="source-url" name="source_url" required class="pve-input"
placeholder="https://example.com/image.7z">
<span class="pve-hint">Direct URL to a disk image or archive (.vmdk, .7z, .zip, etc.)</span>
</div>
<button type="submit" id="start-btn" class="pve-btn pve-btn-primary">Download &amp; Analyse</button>
</form>
<hr class="pve-divider">
<p class="pve-dim" style="font-size:0.78rem; margin-bottom:0.5rem;">
Files on a remote server? Pull directly via SCP:
</p>
<a href="/scp" class="pve-btn">SCP Pull</a>
<!-- Progress area -->
<div id="session-status" class="pve-hidden" style="margin-top:1rem;">
<div id="phase-label" style="margin-bottom:0.5rem;"></div>
<div class="pve-progress">
<div class="pve-progress-bar" id="pve-progress-bar" style="width:0%">0%</div>
</div>
<div id="session-error"></div>
</div>
</div>
</div>
<div id="analysis-section"></div>
<div id="polling-section"></div>
<script>
function setPhase(label, pct, isSpinner) {
const el = document.getElementById('phase-label');
const fill = document.getElementById('pve-progress-bar');
if (el) el.innerHTML = (isSpinner ? '<span class="pve-spinner"></span> ' : '') + label;
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct > 0 ? pct + '%' : ''; }
}
function showError(msg) {
const el = document.getElementById('session-error');
if (el) el.innerHTML += '<div class="pve-alert pve-alert-error" style="margin-top:0.5rem;">' + msg + '</div>';
}
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
async function startSession(e) {
e.preventDefault();
const btn = document.getElementById('start-btn');
const status = document.getElementById('session-status');
const analysis = document.getElementById('analysis-section');
const polling = document.getElementById('polling-section');
btn.disabled = true;
analysis.innerHTML = '';
polling.innerHTML = '';
document.getElementById('session-error').textContent = '';
const vmid = document.getElementById('vmid').value;
const vmName = document.getElementById('vm-name').value.trim();
const url = document.getElementById('source-url').value.trim();
if (!url) { showError('Please enter a download URL.'); btn.disabled = false; return; }
const sessionId = window.VM_BENCH_SID || '';
const formData = new FormData();
formData.append('vmid', vmid);
formData.append('vm_name', vmName);
formData.append('source_type', 'url');
formData.append('source_url', url);
formData.append('session_id', sessionId);
status.classList.remove('pve-hidden');
setPhase('Starting download...', 0, true);
try { await handleDownload(formData); }
catch (err) { setPhase('Download failed', 0, false); showError(err.message); }
btn.disabled = false;
}
async function handleDownload(formData) {
let resp;
try { resp = await fetch('/session/upload', { method: 'POST', body: formData }); }
catch (err) { throw new Error('Network error: ' + err.message); }
const data = await resp.json();
if (data.phase === 'error') { throw new Error(data.error); }
if (data.phase !== 'downloading') { throw new Error('Unexpected: ' + data.phase); }
const filename = data.filename;
setPhase('Downloading source file...', 0, true);
for (;;) {
await sleep(2000);
try {
const pr = await fetch('/session/progress/' + encodeURIComponent(filename));
const pdata = await pr.json();
if (pdata.phase === 'complete') {
setPhase('Download complete (' + (pdata.file_size_gb || 0) + ' GiB)', 100, false);
await sleep(500);
await runAnalysis(data.vmid, filename, data.vm_name);
return;
}
if (pdata.phase === 'error') { throw new Error(pdata.error); }
if (pdata.phase === 'too_slow') {
throw new Error('<strong>Download too slow.</strong><br><br>' + (pdata.message || '') +
'<br><br>Try downloading to a server and using SCP Pull instead.');
}
if (pdata.phase === 'downloading') {
const gb = (pdata.file_size_gb || 0).toFixed(1);
let label = 'Downloading source file... ' + gb + ' GiB';
if (pdata.speed_mbps) label += ' (' + pdata.speed_mbps + ' MB/s)';
if (pdata.eta) label += ' — ' + pdata.eta;
const totalGb = pdata.content_length_gb;
const pct = totalGb ? Math.round((pdata.file_size_gb / totalGb) * 100) : 0;
setPhase(label, pct, totalGb ? false : true);
}
} catch (_) {}
}
}
async function runAnalysis(vmid, filename, vmName) {
setPhase('Step 2/2: Analysing source image...', 0, true);
const fd = new FormData();
fd.append('vmid', vmid); fd.append('filename', filename); fd.append('vm_name', vmName);
fd.append('session_id', window.VM_BENCH_SID || '');
const resp = await fetch('/session/analyze', { method: 'POST', body: fd });
const html = await resp.text();
document.getElementById('analysis-section').innerHTML = html;
document.getElementById('session-status').classList.add('pve-hidden');
initConfirmForm();
}
function initConfirmForm() {
const form = document.getElementById('confirm-form');
if (!form) return;
form.addEventListener('submit', submitJob);
const bootSel = form.querySelector('select[name="auto_detect_boot"]');
if (bootSel) {
bootSel.addEventListener('change', function() {
document.getElementById('boot-type-group').style.display =
this.value === 'false' ? 'block' : 'none';
});
}
}
async function submitJob(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('#submit-btn');
const status = document.getElementById('submit-status');
btn.disabled = true;
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Submitting job...';
const formData = new FormData(form);
formData.append('session_id', window.VM_BENCH_SID || '');
try {
const resp = await fetch('/session/confirm', { method: 'POST', body: formData });
const html = await resp.text();
document.getElementById('step1').style.display = 'none';
document.getElementById('analysis-section').innerHTML = '';
document.getElementById('polling-section').innerHTML = html;
initPolling();
} catch (err) {
status.innerHTML = '<div class="pve-alert pve-alert-error">Failed: ' + err.message + '</div>';
btn.disabled = false;
}
}
let _pollTimer = null;
function initPolling() {
const container = document.getElementById('polling-container');
if (!container) return;
const jobId = container.dataset.jobId;
const vmid = container.dataset.vmid;
const sourceFilename = container.dataset.sourceFilename;
const startTime = Date.now();
let completed = false;
document.getElementById('btn-clone').onclick = () => cloneVM(jobId, vmid);
document.getElementById('btn-copy').onclick = () => {
document.getElementById('copy-options').classList.remove('pve-hidden');
};
document.getElementById('btn-cleanup').onclick = () => reuseImage(jobId, vmid, sourceFilename, false);
document.getElementById('btn-copy-start').onclick = () => copyVM(jobId, vmid, sourceFilename);
const prevVmid = parseInt(vmid) || 21000;
document.getElementById('reuse-vmid').value = prevVmid + 1;
document.getElementById('reuse-vmname').value = (sourceFilename.split('.')[0] || 'vm') + '-2';
async function poll() {
try {
const resp = await fetch('/session/status/' + jobId);
const data = await resp.json();
const pct = data.progress_percentage || 0;
const fill = document.getElementById('pve-progress-bar');
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct + '%'; }
const elMsg = document.getElementById('status-message');
if (elMsg) elMsg.textContent = data.message || '';
document.getElementById('elapsed').textContent = 'Elapsed: ' + Math.round((Date.now() - startTime) / 1000) + 's';
const badge = document.getElementById('status-badge');
const map = { 'queued': ['pve-badge pve-badge-queued', 'Queued'], 'processing_conversion': ['pve-badge pve-badge-running', 'Converting...'], 'importing_storage': ['pve-badge pve-badge-running', 'Importing...'], 'completed': ['pve-badge pve-badge-completed', 'Completed'], 'failed': ['pve-badge pve-badge-failed', 'Failed'] };
const [cls, text] = map[data.status] || ['pve-badge pve-badge-queued', data.status];
if (badge) { badge.className = 'badge ' + cls; badge.innerHTML = (data.status === 'processing_conversion' || data.status === 'importing_storage') ? '<span class="pve-spinner"></span> ' + text : text; }
if (data.error_details) { const eb = document.getElementById('error-block'); if (eb) { eb.classList.remove('pve-hidden'); eb.textContent = data.error_details; } }
if (data.status === 'completed' || data.status === 'failed') { completed = true; if (data.status === 'completed') { const rs = document.getElementById('reuse-section'); if (rs) { rs.classList.remove('pve-hidden'); rs.scrollIntoView({ behavior: 'smooth' }); } } return; }
if (!completed) _pollTimer = setTimeout(poll, 2000);
} catch (err) { const eb = document.getElementById('error-block'); if (eb) { eb.classList.remove('pve-hidden'); eb.textContent = 'Polling error: ' + err.message; } if (!completed) _pollTimer = setTimeout(poll, 5000); }
}
_pollTimer = setTimeout(poll, 3000);
}
async function reuseImage(jobId, vmid, sourceFilename, keep) {
await fetch('/session/cleanup/' + jobId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ delete_staging_files: true, session_id: window.VM_BENCH_SID || '' }) });
window.location.href = '/';
}
async function cloneVM(jobId, sourceVmid) {
const newVmid = parseInt(document.getElementById('reuse-vmid').value) || 0;
const newName = document.getElementById('reuse-vmname').value.trim();
if (!newVmid || !newName) { alert('Please fill VM Name and ID.'); return; }
const status = document.getElementById('reuse-submit-status');
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Cloning VM...';
try {
const resp = await fetch('/session/clone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_vmid: sourceVmid, target_vmid: newVmid, target_name: newName }) });
const data = await resp.json();
status.innerHTML = data.status === 'completed' ? '<div class="pve-alert pve-alert-success">VM ' + newVmid + ' cloned!</div>' : '<div class="pve-alert pve-alert-error">Clone failed: ' + (data.error_details || 'Unknown') + '</div>';
} catch (err) { status.innerHTML = '<div class="pve-alert pve-alert-error">Clone failed: ' + err.message + '</div>'; }
}
async function copyVM(jobId, vmid, sourceFilename) {
const newVmid = parseInt(document.getElementById('reuse-vmid').value) || 0;
const newName = document.getElementById('reuse-vmname').value.trim();
if (!newVmid || !newName) { alert('Please fill VM Name and ID.'); return; }
const status = document.getElementById('copy-status');
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Submitting job...';
const payload = new FormData();
payload.append('vmid', newVmid); payload.append('vm_name', newName);
payload.append('source_filename', sourceFilename);
payload.append('disk_format', 'vmdk');
payload.append('cpu_cores', document.getElementById('copy-cores').value || '2');
payload.append('ram_mb', document.getElementById('copy-ram').value || '4096');
payload.append('target_storage', document.getElementById('copy-storage').value || 'local-lvm');
const diskGb = document.getElementById('copy-disk').value;
if (diskGb) payload.append('target_disk_size_gb', diskGb);
payload.append('auto_detect_boot', 'true');
payload.append('boot_type', 'legacy');
payload.append('session_id', window.VM_BENCH_SID || '');
try {
const resp = await fetch('/session/confirm', { method: 'POST', body: payload });
document.getElementById('polling-section').innerHTML = await resp.text();
initPolling();
} catch (err) { status.innerHTML = '<div class="pve-alert pve-alert-error">Failed: ' + err.message + '</div>'; }
}
</script>
{% endblock %}