feat: landing page with Download, SCP Pull, and Resume cards

- Hero header at top describing the app
- Two side-by-side cards: Download (URL) and SCP Pull
- Resume sessions section appears above cards when sessions exist
- Download card expands to inline form
- Resume asks for VM Name/ID only, skips URL field
- Clean separation of entry points
This commit is contained in:
Claus Lohmar 2026-07-27 07:48:14 +00:00
parent 162110b4b1
commit df6b8085ca
2 changed files with 125 additions and 41 deletions

View file

@ -293,3 +293,50 @@ body {
.pve-divider { border: none; border-top: 1px solid var(--pve-border); margin: 1rem 0; }
.pve-code { background: #f5f5f5; padding: 0.1rem 0.4rem; border-radius: 2px; font-size: 0.78rem; }
.pve-center { text-align: center; }
/* ── Landing page ─────────────────────────────────────────────── */
.pve-hero {
text-align: center;
padding: 1.8rem 1rem 1.2rem;
}
.pve-hero h1 {
font-size: 1.5rem;
font-weight: 700;
color: var(--pve-text);
margin-bottom: 0.3rem;
}
.pve-hero p {
font-size: 0.82rem;
color: var(--pve-muted);
}
.landing-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.25rem;
}
@media (max-width: 520px) { .landing-cards { grid-template-columns: 1fr; } }
.landing-card {
background: var(--pve-panel-bg);
border: 1px solid var(--pve-border);
border-radius: var(--pve-radius);
padding: 1.2rem 1rem;
text-align: center;
cursor: default;
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.landing-card:hover { border-color: var(--pve-accent); }
.landing-card-icon { font-size: 1.6rem; margin-bottom: 0.4rem; }
.landing-card h3 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.landing-card p {
font-size: 0.75rem;
color: var(--pve-muted);
margin-bottom: 0.75rem;
line-height: 1.4;
}

View file

@ -1,52 +1,62 @@
{% extends "base.html" %}
{% block content %}
<div class="pve-hero">
<h1>VM Bench</h1>
<p>Convert VMware images to Proxmox VMs &mdash; automatic OS detection, disk shrink, EFI probing.</p>
</div>
<div id="resume-sessions" class="pve-panel pve-hidden">
<div class="pve-panel-header">
<span class="pve-panel-title">Resume Previous Session</span>
<span class="pve-panel-title">Resume Previous Sessions</span>
</div>
<div class="pve-panel-body" id="resume-sessions-list">
<div class="pve-panel-body" id="resume-sessions-list"></div>
</div>
<div class="landing-cards">
<div class="landing-card" id="card-download">
<div class="landing-card-icon">&#x1F4E5;</div>
<h3>Download</h3>
<p>Pull a disk image or archive from a URL via high-speed download.</p>
<button class="pve-btn pve-btn-primary" onclick="showDownload()">Start</button>
</div>
<div class="landing-card" id="card-scp">
<div class="landing-card-icon">&#x1F4E1;</div>
<h3>SCP Pull</h3>
<p>Transfer files directly from a remote server via SCP &mdash; no middle hop.</p>
<a href="/scp" class="pve-btn pve-btn-primary">Start</a>
</div>
</div>
<div class="pve-panel" id="step1">
<div id="download-section" class="pve-panel pve-hidden">
<div class="pve-panel-header">
<span class="pve-panel-title">New Conversion Session</span>
<span class="pve-panel-title" id="download-title">New Download 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 class="pve-row">
<div class="pve-form-group">
<label>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 '' }}">
</div>
<div class="pve-form-group">
<label>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 21000&ndash;21100. Must be unique on host.</span>
</div>
</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>
<div class="pve-form-group" id="url-group">
<label>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>
<button type="button" class="pve-btn" onclick="hideDownload()" style="margin-left:0.5rem;">Cancel</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">
@ -63,6 +73,24 @@
<script>
loadSessions();
function showDownload() {
document.getElementById('card-download').style.display = 'none';
document.getElementById('card-scp').style.display = 'none';
document.getElementById('download-section').classList.remove('pve-hidden');
document.getElementById('download-title').textContent = 'New Download Session';
document.getElementById('url-group').classList.remove('pve-hidden');
document.getElementById('start-btn').textContent = 'Download & Analyse';
document.getElementById('start-btn').onclick = null; // restore form submit
document.getElementById('source-url').required = true;
}
function hideDownload() {
document.getElementById('card-download').style.display = '';
document.getElementById('card-scp').style.display = '';
document.getElementById('download-section').classList.add('pve-hidden');
window.RESUMED_SID = null;
}
async function loadSessions() {
try {
var resp = await fetch('/session/sessions');
@ -89,17 +117,26 @@ async function loadSessions() {
}
function resumeSession(sessionId, firstFile) {
var vmid = document.getElementById('vmid').value;
var vmName = document.getElementById('vm-name').value.trim();
if (!vmid) { alert('Please enter a VM ID first.'); return; }
window.RESUMED_SID = sessionId;
var ses = document.getElementById('resume-sessions');
ses.classList.add('pve-hidden');
document.getElementById('step1').style.display = 'none';
var status = document.getElementById('session-status');
status.classList.remove('pve-hidden');
setPhase('Analysing source image...', 0, true);
runAnalysis(vmid, firstFile, vmName, sessionId);
document.getElementById('card-download').style.display = 'none';
document.getElementById('card-scp').style.display = 'none';
document.getElementById('download-section').classList.remove('pve-hidden');
document.getElementById('download-title').textContent = 'Resume Session';
document.getElementById('url-group').classList.add('pve-hidden');
var btn = document.getElementById('start-btn');
btn.textContent = 'Resume Analysis';
btn.onclick = function(e) {
e.preventDefault();
var vmid = document.getElementById('vmid').value;
var vmName = document.getElementById('vm-name').value.trim();
if (!vmid) { alert('Please enter a VM ID.'); return; }
window.RESUMED_SID = sessionId;
document.getElementById('resume-sessions').classList.add('pve-hidden');
var status = document.getElementById('session-status');
status.classList.remove('pve-hidden');
setPhase('Analysing source image...', 0, true);
runAnalysis(vmid, firstFile, vmName, sessionId);
};
document.getElementById('source-url').required = false;
}
function setPhase(label, pct, isSpinner) {
@ -278,7 +315,7 @@ async function submitJob(e) {
try {
const resp = await fetch('/session/confirm', { method: 'POST', body: formData });
const html = await resp.text();
document.getElementById('step1').style.display = 'none';
document.getElementById('download-section').style.display = 'none';
document.getElementById('analysis-section').innerHTML = '';
document.getElementById('polling-section').innerHTML = html;
initPolling();