style: Proxmox VE-inspired dark theme — pve-* class naming
Rewrote proxmox.css with consistent pve- prefix matching the Proxmox VE design language: panels with orange-accent headers, clean tables, orange progress bars, green/red/orange badges, subtle shadows. Updated all templates (base, index, _analysis, polling, scp) to use pve-btn, pve-input, pve-select, pve-panel, pve-progress, pve-badge, pve-alert, pve-spinner, pve-hidden, pve-dim, etc.
This commit is contained in:
parent
492924f346
commit
844d86c105
6 changed files with 433 additions and 263 deletions
|
|
@ -1,117 +1,281 @@
|
|||
/* Proxmox VE-inspired dark theme for VM Bench */
|
||||
/* ═════════════════════════════════════════════════════════
|
||||
Proxmox VE-inspired dark theme for VM Bench
|
||||
═════════════════════════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
--bg: #161616;
|
||||
--panel: #1e1e1e;
|
||||
--border: #3a3a3a;
|
||||
--text: #e0e0e0;
|
||||
--muted: #999;
|
||||
--accent: #f48024; /* Proxmox orange */
|
||||
--accent-alt: #3892d5; /* blue accent */
|
||||
--green: #54b948;
|
||||
--red: #e8524a;
|
||||
--yellow: #e5a72e;
|
||||
--radius: 4px;
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--pve-bg: #161616;
|
||||
--pve-panel-bg: #1e1e1e;
|
||||
--pve-border: #3a3a3a;
|
||||
--pve-text: #e0e0e0;
|
||||
--pve-muted: #888;
|
||||
--pve-accent: #f48024; /* Proxmox orange */
|
||||
--pve-blue: #3892d5;
|
||||
--pve-green: #54b948;
|
||||
--pve-red: #e8524a;
|
||||
--pve-yellow: #e5a72e;
|
||||
--pve-radius: 4px;
|
||||
--pve-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg); color: var(--text); font-family: var(--font);
|
||||
font-size: 13px; line-height: 1.5; min-height: 100vh;
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--pve-bg);
|
||||
color: var(--pve-text);
|
||||
font-family: var(--pve-font);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
header {
|
||||
background: var(--panel); border-bottom: 2px solid var(--accent);
|
||||
padding: 0.75rem 1.5rem; display: flex; align-items: baseline; gap: 1rem;
|
||||
}
|
||||
header .logo { font-size: 1.2rem; font-weight: 700; color: var(--accent); }
|
||||
header .sub { font-size: 0.8rem; color: var(--muted); }
|
||||
footer {
|
||||
margin-top: auto; padding: 0.5rem 1.5rem; font-size: 0.75rem;
|
||||
color: var(--muted); display: flex; justify-content: space-between;
|
||||
border-top: 1px solid var(--border); background: var(--panel);
|
||||
}
|
||||
.container { max-width: 760px; margin: 0 auto; padding: 2rem 1rem; width: 100%; }
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 1.5rem; margin-bottom: 1.5rem;
|
||||
/* ── Header ──────────────────────────────────────────────────── */
|
||||
.pve-header {
|
||||
background: var(--pve-panel-bg);
|
||||
border-bottom: 2px solid var(--pve-accent);
|
||||
padding: 0.6rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.panel h2 {
|
||||
font-size: 1rem; color: var(--accent); margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
|
||||
.pve-header .pve-logo {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--pve-accent);
|
||||
}
|
||||
.pve-header .pve-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--pve-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
.poll-panel { text-align: center; }
|
||||
|
||||
/* Forms */
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
label { display: block; margin-bottom: 0.25rem; font-size: 0.8rem; color: var(--muted); }
|
||||
input, select, button, textarea {
|
||||
width: 100%; padding: 0.5rem 0.75rem; background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
color: var(--text); font-family: inherit; font-size: 0.85rem;
|
||||
/* ── Footer ──────────────────────────────────────────────────── */
|
||||
.pve-footer {
|
||||
margin-top: auto;
|
||||
padding: 0.4rem 1.5rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--pve-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--pve-border);
|
||||
background: var(--pve-panel-bg);
|
||||
}
|
||||
|
||||
/* ── Container ───────────────────────────────────────────────── */
|
||||
.pve-container {
|
||||
max-width: 780px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Panels ──────────────────────────────────────────────────── */
|
||||
.pve-panel {
|
||||
background: var(--pve-panel-bg);
|
||||
border: 1px solid var(--pve-border);
|
||||
border-radius: var(--pve-radius);
|
||||
margin-bottom: 1.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pve-panel-header {
|
||||
background: #252525;
|
||||
padding: 0.65rem 1.25rem;
|
||||
border-bottom: 1px solid var(--pve-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.pve-panel-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--pve-text);
|
||||
}
|
||||
.pve-panel-title::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: var(--pve-accent);
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.pve-panel-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
.pve-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.45rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--pve-border);
|
||||
border-radius: var(--pve-radius);
|
||||
background: #2a2a2a;
|
||||
color: var(--pve-text);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pve-btn:hover { background: #333; border-color: #555; }
|
||||
.pve-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.pve-btn-primary { background: var(--pve-accent); color: #fff; border-color: var(--pve-accent); }
|
||||
.pve-btn-primary:hover { background: #e07a1e; }
|
||||
|
||||
.pve-btn-success { background: var(--pve-green); color: #fff; border-color: var(--pve-green); }
|
||||
.pve-btn-success:hover { background: #4aa540; }
|
||||
|
||||
.pve-btn-danger { background: var(--pve-red); color: #fff; border-color: var(--pve-red); }
|
||||
.pve-btn-danger:hover { background: #d9443f; }
|
||||
|
||||
.pve-btn-small { padding: 0.25rem 0.6rem; font-size: 0.72rem; }
|
||||
|
||||
.pve-btn-row {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Forms ───────────────────────────────────────────────────── */
|
||||
.pve-form-group { margin-bottom: 0.85rem; }
|
||||
.pve-form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--pve-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.pve-input, .pve-select {
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.65rem;
|
||||
background: var(--pve-bg);
|
||||
border: 1px solid var(--pve-border);
|
||||
border-radius: var(--pve-radius);
|
||||
color: var(--pve-text);
|
||||
font-family: inherit;
|
||||
font-size: 0.82rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); outline: none; }
|
||||
.hint { display: block; font-size: 0.7rem; color: var(--muted); margin-top: 0.25rem; }
|
||||
.hidden { display: none !important; }
|
||||
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
@media (max-width: 520px) { .row { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Buttons */
|
||||
button, .btn {
|
||||
display: inline-block; padding: 0.5rem 1.25rem; font-weight: 600;
|
||||
font-size: 0.85rem; cursor: pointer; text-decoration: none;
|
||||
border-radius: var(--radius); transition: opacity 0.15s;
|
||||
.pve-input:focus, .pve-select:focus {
|
||||
border-color: var(--pve-accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(244,128,36,0.15);
|
||||
}
|
||||
button, .btn { background: var(--accent); color: #fff; border: none; }
|
||||
button:hover, .btn:hover { opacity: 0.85; }
|
||||
.btn-secondary, button.btn-secondary { background: var(--border); color: var(--text); }
|
||||
.btn-row { display: flex; gap: 0.75rem; margin-top: 1rem; }
|
||||
|
||||
/* Tables */
|
||||
table { width: 100%; border-collapse: collapse; margin: 0.75rem 0; }
|
||||
td { padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
||||
td:first-child { color: var(--muted); width: 35%; }
|
||||
td:last-child { font-weight: 500; }
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar {
|
||||
height: 1.25rem; background: var(--bg); border-radius: var(--radius);
|
||||
overflow: hidden; margin: 1rem 0;
|
||||
.pve-hint {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
color: var(--pve-muted);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; background: var(--accent); transition: width 0.4s ease;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.7rem; font-weight: 700; color: #fff; min-width: 2.5rem;
|
||||
.pve-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
@media (max-width: 520px) { .pve-row { grid-template-columns: 1fr; } }
|
||||
|
||||
.pve-hidden { display: none !important; }
|
||||
|
||||
/* ── Tables ──────────────────────────────────────────────────── */
|
||||
.pve-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.pve-table td {
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-bottom: 1px solid var(--pve-border);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.pve-table td:first-child {
|
||||
color: var(--pve-muted);
|
||||
width: 35%;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pve-table td:last-child { font-weight: 500; }
|
||||
|
||||
/* ── Progress ────────────────────────────────────────────────── */
|
||||
.pve-progress {
|
||||
height: 1.15rem;
|
||||
background: var(--pve-bg);
|
||||
border-radius: var(--pve-radius);
|
||||
overflow: hidden;
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
.pve-progress-bar {
|
||||
height: 100%;
|
||||
background: var(--pve-accent);
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
min-width: 2.5rem;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block; padding: 0.2rem 0.6rem; border-radius: 3px;
|
||||
font-size: 0.75rem; font-weight: 600; margin-bottom: 0.75rem;
|
||||
/* ── Badges ──────────────────────────────────────────────────── */
|
||||
.pve-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.badge-queued { background: #3b3410; color: var(--yellow); }
|
||||
.badge-running { background: #1e3a5f; color: var(--accent-alt); }
|
||||
.badge-completed { background: #1a3a1a; color: var(--green); }
|
||||
.badge-failed { background: #3a1a1a; color: var(--red); }
|
||||
.pve-badge-queued { background: #3b3410; color: var(--pve-yellow); }
|
||||
.pve-badge-running { background: #1e3a5f; color: var(--pve-blue); }
|
||||
.pve-badge-completed { background: #1a3a1a; color: var(--pve-green); }
|
||||
.pve-badge-failed { background: #3a1a1a; color: var(--pve-red); }
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
display: inline-block; width: 0.85rem; height: 0.85rem;
|
||||
border: 2px solid var(--border); border-top-color: currentColor;
|
||||
border-radius: 50%; animation: spin 0.6s linear infinite; vertical-align: middle; margin-right: 0.25rem;
|
||||
/* ── Spinner ─────────────────────────────────────────────────── */
|
||||
.pve-spinner {
|
||||
display: inline-block;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
border: 2px solid var(--pve-border);
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: pve-spin 0.6s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pve-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Alerts */
|
||||
.error { background: #3a1a1a; border: 1px solid var(--red); color: var(--red);
|
||||
padding: 0.6rem 0.75rem; border-radius: var(--radius); font-size: 0.85rem; }
|
||||
.success { background: #1a3a1a; border: 1px solid var(--green); color: var(--green);
|
||||
padding: 0.6rem 0.75rem; border-radius: var(--radius); }
|
||||
.error-panel h2 { color: var(--red); }
|
||||
/* ── Alerts ──────────────────────────────────────────────────── */
|
||||
.pve-alert {
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-radius: var(--pve-radius);
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pve-alert-error {
|
||||
background: #3a1a1a;
|
||||
border: 1px solid var(--pve-red);
|
||||
color: var(--pve-red);
|
||||
}
|
||||
.pve-alert-success {
|
||||
background: #1a3a1a;
|
||||
border: 1px solid var(--pve-green);
|
||||
color: var(--pve-green);
|
||||
}
|
||||
.pve-alert-info {
|
||||
background: #1a2a3a;
|
||||
border: 1px solid var(--pve-blue);
|
||||
color: var(--pve-blue);
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
.dim { color: var(--muted); }
|
||||
.confirm-text { color: var(--muted); font-size: 0.85rem; margin: 0.75rem 0; }
|
||||
code { background: var(--bg); padding: 0.1rem 0.4rem; border-radius: 2px; font-size: 0.8rem; }
|
||||
/* ── Misc ────────────────────────────────────────────────────── */
|
||||
.pve-dim { color: var(--pve-muted); }
|
||||
.pve-divider { border: none; border-top: 1px solid var(--pve-border); margin: 1rem 0; }
|
||||
.pve-code { background: var(--pve-bg); padding: 0.1rem 0.4rem; border-radius: 2px; font-size: 0.78rem; }
|
||||
.pve-center { text-align: center; }
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
{# Fragment — no <script> tags (injected via innerHTML) #}
|
||||
|
||||
{% if error %}
|
||||
<div class="panel error-panel" id="analysis-result">
|
||||
<h2>Analysis Failed</h2>
|
||||
<div class="error">{{ error }}</div>
|
||||
<a href="/" class="btn">Try Again</a>
|
||||
<div class="pve-panel" id="analysis-result">
|
||||
<div class="pve-panel-header">
|
||||
<span class="pve-panel-title">Analysis Failed</span>
|
||||
</div>
|
||||
<div class="pve-panel-body">
|
||||
<div class="pve-alert pve-alert-error">{{ error }}</div>
|
||||
<a href="/" class="pve-btn" style="margin-top:0.75rem;">Try Again</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel" id="step2">
|
||||
<h2>Analysis Result</h2>
|
||||
<div class="pve-panel" id="step2">
|
||||
<div class="pve-panel-header">
|
||||
<span class="pve-panel-title">Analysis Result</span>
|
||||
</div>
|
||||
<div class="pve-panel-body">
|
||||
|
||||
<table>
|
||||
<table class="pve-table">
|
||||
<tr><td>File</td><td>{{ analysis.filename }}</td></tr>
|
||||
<tr><td>Disk Format</td><td>{{ analysis.disk_format }}</td></tr>
|
||||
<tr><td>Disk Size</td><td>{{ analysis.disk_size_gb }} GB</td></tr>
|
||||
|
|
@ -18,61 +25,62 @@
|
|||
<tr><td>EFI Bootable</td><td>{{ 'Yes' if analysis.efi_detectable else 'Unknown' }}</td></tr>
|
||||
</table>
|
||||
|
||||
<p class="confirm-text">Is this correct? Configure the VM below and submit the conversion job.</p>
|
||||
<p class="pve-dim" style="margin:0.75rem 0;">Is this correct? Configure the VM below and submit the conversion job.</p>
|
||||
|
||||
<form id="confirm-form" data-vmid="{{ vmid }}" data-source-filename="{{ source_filename }}">
|
||||
<input type="hidden" name="vmid" value="{{ vmid }}">
|
||||
<input type="hidden" name="source_filename" value="{{ source_filename }}">
|
||||
<input type="hidden" name="disk_format" value="{{ analysis.disk_format }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>VM Name *</label>
|
||||
<input type="text" name="vm_name" required value="{{ vm_name or '' }}"
|
||||
<input type="text" name="vm_name" required class="pve-input" value="{{ vm_name or '' }}"
|
||||
placeholder="my-converted-vm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Target Disk Size (GB)</label>
|
||||
<input type="number" name="target_disk_size_gb"
|
||||
<input type="number" name="target_disk_size_gb" class="pve-input"
|
||||
value="{{ analysis.disk_size_gb }}"
|
||||
placeholder="Leave blank for auto-shrink">
|
||||
<span class="hint">{{ analysis.disk_size_gb }} GB detected. Omit to auto-shrink to 30 GB.</span>
|
||||
<span class="pve-hint">{{ analysis.disk_size_gb }} GB detected. Omit to auto-shrink to 30 GB.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>CPU Cores</label>
|
||||
<input type="number" name="cpu_cores" value="2" min="1">
|
||||
<input type="number" name="cpu_cores" class="pve-input" value="2" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>RAM (MB)</label>
|
||||
<input type="number" name="ram_mb" value="4096" min="512">
|
||||
<input type="number" name="ram_mb" class="pve-input" value="4096" min="512">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>Storage Pool</label>
|
||||
<input type="text" name="target_storage" value="local-lvm">
|
||||
<input type="text" name="target_storage" class="pve-input" value="local-lvm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Boot Detection</label>
|
||||
<select name="auto_detect_boot">
|
||||
<select name="auto_detect_boot" class="pve-select">
|
||||
<option value="true" selected>Auto-detect (recommended)</option>
|
||||
<option value="false">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="boot-type-group" style="display:none;">
|
||||
<div class="pve-form-group" id="boot-type-group" style="display:none;">
|
||||
<label>Boot Type</label>
|
||||
<select name="boot_type">
|
||||
<select name="boot_type" class="pve-select">
|
||||
<option value="uefi">UEFI</option>
|
||||
<option value="legacy">Legacy BIOS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submit-btn">Start Conversion</button>
|
||||
<button type="submit" id="submit-btn" class="pve-btn pve-btn-primary">Start Conversion</button>
|
||||
</form>
|
||||
|
||||
<div id="submit-status" class="hidden" style="margin-top:1rem;"></div>
|
||||
<div id="submit-status" class="pve-hidden" style="margin-top:1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo">VM Bench</div>
|
||||
<div class="sub">Proxmox Image Conversion</div>
|
||||
<header class="pve-header">
|
||||
<div class="pve-logo">VM Bench</div>
|
||||
<div class="pve-sub">Proxmox Image Conversion</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="pve-container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<footer class="pve-footer">
|
||||
<span>vm-bench frontend</span>
|
||||
<span>Backend: {{ backend_url or "10.2.0.2:9000" }}</span>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -2,64 +2,65 @@
|
|||
{% block content %}
|
||||
|
||||
<!-- Step 1: Start New Session -->
|
||||
<div class="panel" id="step1">
|
||||
<h2>New Conversion Session</h2>
|
||||
<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="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label for="vm-name">VM Name *</label>
|
||||
<input type="text" id="vm-name" name="vm_name" required
|
||||
<input type="text" id="vm-name" name="vm_name" required class="pve-input"
|
||||
placeholder="e.g. debian-test" value="{{ prefill_vmname or '' }}">
|
||||
<span class="hint">Display name for the VM on the Proxmox host.</span>
|
||||
<span class="pve-hint">Display name for the VM on the Proxmox host.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label for="vmid">VM ID *</label>
|
||||
<input type="number" id="vmid" name="vmid" required
|
||||
<input type="number" id="vmid" name="vmid" required class="pve-input"
|
||||
placeholder="e.g. 21050" min="21000" max="21100">
|
||||
<span class="hint">Range 21000–21100. Must be unique on the Proxmox host.</span>
|
||||
<span class="pve-hint">Range 21000–21100. Must be unique on the Proxmox host.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label for="source-type">Source Type</label>
|
||||
<select id="source-type" onchange="toggleSourceInput(event)">
|
||||
<select id="source-type" class="pve-select" onchange="toggleSourceInput(event)">
|
||||
<option value="upload">File Upload</option>
|
||||
<option value="url">Download from URL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="upload-group">
|
||||
<div class="pve-form-group" id="upload-group">
|
||||
<label for="source-file">Source File (.vmdk / .7z / .zip / .tar.gz)</label>
|
||||
<input type="file" id="source-file" name="source_file"
|
||||
<input type="file" id="source-file" name="source_file" class="pve-input"
|
||||
accept=".vmdk,.vhd,.vhdx,.7z,.zip,.tar.gz,.tgz,.tar,.gz">
|
||||
</div>
|
||||
|
||||
<div class="form-group hidden" id="url-group">
|
||||
<div class="pve-form-group pve-hidden" id="url-group">
|
||||
<label for="source-url">Download URL</label>
|
||||
<input type="url" id="source-url" name="source_url"
|
||||
<input type="url" id="source-url" name="source_url" class="pve-input"
|
||||
placeholder="https://example.com/image.7z">
|
||||
</div>
|
||||
|
||||
<button type="submit" id="start-btn">Analyse Source Image</button>
|
||||
<button type="submit" id="start-btn" class="pve-btn pve-btn-primary">Analyse Source Image</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--border);">
|
||||
<p style="color:var(--muted); font-size:0.8rem; margin-bottom:0.5rem;">
|
||||
For files larger than 10 GB or on remote servers — pull directly via SCP:
|
||||
</p>
|
||||
<a href="/scp" class="btn btn-secondary" style="width:auto; display:inline-block;">
|
||||
SCP Pull (Large Files +10 GB)
|
||||
</a>
|
||||
</div>
|
||||
<hr class="pve-divider">
|
||||
<p class="pve-dim" style="font-size:0.78rem; margin-bottom:0.5rem;">
|
||||
For files larger than 10 GB or on remote servers — pull directly via SCP:
|
||||
</p>
|
||||
<a href="/scp" class="pve-btn">SCP Pull (Large Files +10 GB)</a>
|
||||
|
||||
<!-- Progress area (hidden until form submit) -->
|
||||
<div id="session-status" class="hidden" style="margin-top:1rem;">
|
||||
<div id="session-status" class="pve-hidden" style="margin-top:1rem;">
|
||||
<div id="phase-label" style="margin-bottom:0.5rem;"></div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-fill" style="width:0%">0%</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>
|
||||
|
||||
<!-- Step 2: Analysis Result (populated by JavaScript) -->
|
||||
|
|
@ -72,14 +73,14 @@
|
|||
// ── Source type toggle ────────────────────────────────────────────
|
||||
function toggleSourceInput(e) {
|
||||
const type = e.target.value;
|
||||
document.getElementById('upload-group').classList.toggle('hidden', type !== 'upload');
|
||||
document.getElementById('url-group').classList.toggle('hidden', type !== 'url');
|
||||
document.getElementById('upload-group').classList.toggle('pve-hidden', type !== 'upload');
|
||||
document.getElementById('url-group').classList.toggle('pve-hidden', type !== 'url');
|
||||
}
|
||||
|
||||
// ── Progress helpers ──────────────────────────────────────────────
|
||||
function setPhase(label, pct, isSpinner) {
|
||||
const el = document.getElementById('phase-label');
|
||||
const fill = document.getElementById('progress-fill');
|
||||
const fill = document.getElementById('pve-progress-bar');
|
||||
if (el) el.innerHTML = (isSpinner ? '<span class="spinner"></span> ' : '') + label;
|
||||
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct > 0 ? pct + '%' : ''; }
|
||||
}
|
||||
|
|
@ -103,10 +104,10 @@ async function startSession(e) {
|
|||
polling.innerHTML = '';
|
||||
// Reset permanent elements (no innerHTML — keeps DOM references alive)
|
||||
document.getElementById('phase-label').textContent = '';
|
||||
var pf = document.getElementById('progress-fill');
|
||||
var pf = document.getElementById('pve-progress-bar');
|
||||
pf.style.width = '0%'; pf.textContent = '0%';
|
||||
document.getElementById('session-error').textContent = '';
|
||||
status.classList.remove('hidden');
|
||||
status.classList.remove('pve-hidden');
|
||||
|
||||
const vmid = document.getElementById('vmid').value;
|
||||
const vmName = document.getElementById('vm-name').value.trim();
|
||||
|
|
@ -181,7 +182,7 @@ async function handleUpload(formData) {
|
|||
}
|
||||
resolve();
|
||||
});
|
||||
xhr.addEventListener('error', () => { setPhase('Upload failed', 0, false); showError('Network error.'); resolve(); });
|
||||
xhr.addEventListener('pve-alert pve-alert-error', () => { setPhase('Upload failed', 0, false); showError('Network error.'); resolve(); });
|
||||
xhr.send(file); // raw binary — no multipart overhead
|
||||
});
|
||||
}
|
||||
|
|
@ -192,7 +193,7 @@ async function handleDownload(formData) {
|
|||
try { resp = await fetch('/session/upload', { method: 'POST', body: formData }); }
|
||||
catch (err) { setPhase('Download failed', 0, false); showError('Network error: ' + err.message); return; }
|
||||
const data = await resp.json();
|
||||
if (data.phase === 'error') { setPhase('Download failed', 0, false); showError(data.error); return; }
|
||||
if (data.phase === 'pve-alert pve-alert-error') { setPhase('Download failed', 0, false); showError(data.error); return; }
|
||||
if (data.phase !== 'downloading') { setPhase('Unexpected: ' + data.phase, 0, false); return; }
|
||||
|
||||
const filename = data.filename;
|
||||
|
|
@ -206,7 +207,7 @@ async function handleDownload(formData) {
|
|||
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') { setPhase('Download failed', 0, false); showError(pdata.error); return; }
|
||||
if (pdata.phase === 'pve-alert pve-alert-error') { setPhase('Download failed', 0, false); showError(pdata.error); return; }
|
||||
if (pdata.phase === 'too_slow') {
|
||||
setPhase('Download too slow', 0, false);
|
||||
showError('<strong>Download cancelled — too slow.</strong><br><br>' + (pdata.message || '') +
|
||||
|
|
@ -236,7 +237,7 @@ async function runAnalysis(vmid, filename, vmName) {
|
|||
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('hidden');
|
||||
document.getElementById('session-status').classList.add('pve-hidden');
|
||||
// Attach event listeners since innerHTML doesn't execute <script>
|
||||
initConfirmForm();
|
||||
} catch (err) { setPhase('Analysis failed', 0, false); showError('Failed: ' + err.message); }
|
||||
|
|
@ -263,7 +264,7 @@ async function submitJob(e) {
|
|||
const btn = form.querySelector('#submit-btn');
|
||||
const status = document.getElementById('submit-status');
|
||||
btn.disabled = true;
|
||||
status.classList.remove('hidden');
|
||||
status.classList.remove('pve-hidden');
|
||||
status.innerHTML = '<div class="spinner"></div> Submitting job...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
|
@ -303,7 +304,7 @@ function initPolling() {
|
|||
|
||||
document.getElementById('btn-clone').onclick = () => cloneVM(jobId, vmid);
|
||||
document.getElementById('btn-copy').onclick = () => {
|
||||
document.getElementById('copy-options').classList.remove('hidden');
|
||||
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);
|
||||
|
|
@ -313,7 +314,7 @@ function initPolling() {
|
|||
const resp = await fetch('/session/status/' + jobId);
|
||||
const data = await resp.json();
|
||||
const pct = data.progress_percentage || 0;
|
||||
const fill = document.getElementById('conversion-fill');
|
||||
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 || '';
|
||||
|
|
@ -322,13 +323,13 @@ function initPolling() {
|
|||
|
||||
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'],
|
||||
'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] || ['badge-queued', data.status];
|
||||
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')
|
||||
|
|
@ -337,21 +338,21 @@ function initPolling() {
|
|||
|
||||
if (data.error_details) {
|
||||
const errBlock = document.getElementById('error-block');
|
||||
if (errBlock) { errBlock.classList.remove('hidden'); errBlock.textContent = data.error_details; }
|
||||
if (errBlock) { errBlock.classList.remove('pve-hidden'); errBlock.textContent = data.error_details; }
|
||||
}
|
||||
|
||||
if (data.status === 'completed' || data.status === 'failed') {
|
||||
completed = true;
|
||||
if (data.status === 'completed') {
|
||||
const reuse = document.getElementById('reuse-section');
|
||||
if (reuse) { reuse.classList.remove('hidden'); reuse.scrollIntoView({ behavior: 'smooth' }); }
|
||||
if (reuse) { reuse.classList.remove('pve-hidden'); reuse.scrollIntoView({ behavior: 'smooth' }); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!completed) _pollTimer = setTimeout(poll, 2000);
|
||||
} catch (err) {
|
||||
const errBlock = document.getElementById('error-block');
|
||||
if (errBlock) { errBlock.classList.remove('hidden'); errBlock.textContent = 'Polling error: ' + err.message; }
|
||||
if (errBlock) { errBlock.classList.remove('pve-hidden'); errBlock.textContent = 'Polling error: ' + err.message; }
|
||||
if (!completed) _pollTimer = setTimeout(poll, 5000);
|
||||
}
|
||||
}
|
||||
|
|
@ -375,7 +376,7 @@ async function cloneVM(jobId, sourceVmid) {
|
|||
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('hidden');
|
||||
status.classList.remove('pve-hidden');
|
||||
status.innerHTML = '<div class="spinner"></div> Cloning VM...';
|
||||
try {
|
||||
const resp = await fetch('/session/clone', {
|
||||
|
|
@ -398,7 +399,7 @@ async function copyVM(jobId, vmid, sourceFilename) {
|
|||
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('hidden');
|
||||
status.classList.remove('pve-hidden');
|
||||
status.innerHTML = '<div class="spinner"></div> Submitting job...';
|
||||
const payload = new FormData();
|
||||
payload.append('vmid', newVmid);
|
||||
|
|
|
|||
|
|
@ -1,73 +1,81 @@
|
|||
<div class="container" id="polling-container"
|
||||
<div class="pve-container" id="polling-container"
|
||||
data-job-id="{{ job_id }}" data-vmid="{{ vmid }}"
|
||||
data-source-filename="{{ source_filename }}" data-vm-name="{{ vm_name }}">
|
||||
|
||||
<div class="panel poll-panel">
|
||||
<h2>Converting VM {{ vmid }}</h2>
|
||||
<p class="dim">Job: <code>{{ job_id }}</code> | {{ vm_name }}</p>
|
||||
<div class="pve-panel pve-center">
|
||||
<div class="pve-panel-header">
|
||||
<span class="pve-panel-title">Converting VM {{ vmid }}</span>
|
||||
</div>
|
||||
<div class="pve-panel-body">
|
||||
<p class="pve-dim">Job: <span class="pve-code">{{ job_id }}</span> | {{ vm_name }}</p>
|
||||
|
||||
<div id="status-badge" class="badge badge-queued">
|
||||
<span class="spinner"></span> Queued
|
||||
<div id="status-badge" class="pve-badge pve-badge-queued">
|
||||
<span class="pve-spinner"></span> Queued
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="conversion-fill" style="width:0%">0%</div>
|
||||
<div class="pve-progress">
|
||||
<div class="pve-progress-bar" id="pve-progress-bar" 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>
|
||||
<p id="status-message" class="pve-dim">Waiting for backend...</p>
|
||||
<p id="elapsed" class="pve-dim">Elapsed: 0s</p>
|
||||
<div id="error-block" class="pve-alert pve-alert-error pve-hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reuse section -->
|
||||
<div id="reuse-section" class="panel hidden">
|
||||
<h2>Conversion Complete — VM {{ vmid }} Ready</h2>
|
||||
<div id="reuse-section" class="pve-panel pve-hidden">
|
||||
<div class="pve-panel-header">
|
||||
<span class="pve-panel-title">Conversion Complete — VM {{ vmid }} Ready</span>
|
||||
</div>
|
||||
<div class="pve-panel-body">
|
||||
<p style="margin-bottom:1rem;">Create another VM from this disk?</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>VM Name *</label>
|
||||
<input type="text" id="reuse-vmname" name="vm_name" required
|
||||
<input type="text" id="reuse-vmname" name="vm_name" required class="pve-input"
|
||||
placeholder="e.g. second-vm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>VM ID *</label>
|
||||
<input type="number" id="reuse-vmid" name="vmid" required
|
||||
<input type="number" id="reuse-vmid" name="vmid" required class="pve-input"
|
||||
min="21000" max="21100" placeholder="21051">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="flex-wrap:wrap;">
|
||||
<button id="btn-clone" class="btn" style="background:var(--green);">Clone (instant)</button>
|
||||
<button id="btn-copy" class="btn">Copy (re-convert)</button>
|
||||
<button id="btn-cleanup" class="btn btn-secondary">Clean Up & Finish</button>
|
||||
<div class="pve-btn-row" style="flex-wrap:wrap;">
|
||||
<button id="btn-clone" class="pve-btn pve-btn-success">Clone (instant)</button>
|
||||
<button id="btn-copy" class="pve-btn pve-btn-primary">Copy (re-convert)</button>
|
||||
<button id="btn-cleanup" class="pve-btn">Clean Up & Finish</button>
|
||||
</div>
|
||||
<div id="reuse-submit-status" class="hidden" style="margin-top:0.5rem;"></div>
|
||||
<div id="reuse-submit-status" class="pve-hidden" style="margin-top:0.5rem;"></div>
|
||||
|
||||
<!-- Copy options (hidden until Copy clicked) -->
|
||||
<div id="copy-options" class="hidden" style="margin-top:1rem; padding-top:1rem; border-top:1px solid var(--border);">
|
||||
<p class="dim" style="margin-bottom:0.75rem;">Copy re-runs the conversion with new specs:</p>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div id="copy-options" class="pve-hidden" style="margin-top:1rem; padding-top:1rem; border-top:1px solid var(--pve-border);">
|
||||
<p class="pve-dim" style="margin-bottom:0.75rem;">Copy re-runs the conversion with new specs:</p>
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>CPU Cores</label>
|
||||
<input type="number" id="copy-cores" value="2" min="1">
|
||||
<input type="number" id="copy-cores" class="pve-input" value="2" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>RAM (MB)</label>
|
||||
<input type="number" id="copy-ram" value="4096" min="512">
|
||||
<input type="number" id="copy-ram" class="pve-input" value="4096" min="512">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Target Disk Size (GB)</label>
|
||||
<input type="number" id="copy-disk" value="30" placeholder="Leave blank for auto-shrink">
|
||||
<span class="hint">Blank = auto-shrink to 30 GB.</span>
|
||||
<input type="number" id="copy-disk" class="pve-input" value="30" placeholder="Leave blank for auto-shrink">
|
||||
<span class="pve-hint">Blank = auto-shrink to 30 GB.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Storage Pool</label>
|
||||
<input type="text" id="copy-storage" value="local-lvm">
|
||||
<input type="text" id="copy-storage" class="pve-input" value="local-lvm">
|
||||
</div>
|
||||
<button id="btn-copy-start" class="btn" style="margin-top:0.5rem;">Start Re-Conversion</button>
|
||||
<div id="copy-status" class="hidden" style="margin-top:0.5rem;"></div>
|
||||
<button id="btn-copy-start" class="pve-btn pve-btn-primary" style="margin-top:0.5rem;">Start Re-Conversion</button>
|
||||
<div id="copy-status" class="pve-hidden" style="margin-top:0.5rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,73 +1,74 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="panel" id="scp-form-panel">
|
||||
<h2>SCP Pull — Fetch File from Remote Server</h2>
|
||||
<p class="confirm-text">
|
||||
<div class="pve-panel" id="scp-form-panel">
|
||||
<div class="pve-panel-header">
|
||||
<span class="pve-panel-title">SCP Pull — Fetch File from Remote Server</span>
|
||||
</div>
|
||||
<div class="pve-panel-body">
|
||||
<p class="pve-dim" style="margin-bottom:1rem;">
|
||||
Pull a disk image directly from a customer server via SCP.
|
||||
No laptop middle-hop — files land directly in the staging area.
|
||||
<strong>Supports files of any size.</strong>
|
||||
</p>
|
||||
|
||||
<form id="scp-form" onsubmit="startScpPull(event)">
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>Remote Host</label>
|
||||
<input type="text" name="scp_host" required
|
||||
<input type="text" name="scp_host" required class="pve-input"
|
||||
placeholder="10.0.0.50 or server.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>SSH Port</label>
|
||||
<input type="number" name="scp_port" value="22" min="1" max="65535">
|
||||
<input type="number" name="scp_port" class="pve-input" value="22" min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" name="scp_user" required placeholder="root">
|
||||
<input type="text" name="scp_user" required class="pve-input" placeholder="root">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" name="scp_pass" required placeholder="••••••••">
|
||||
<input type="password" name="scp_pass" required class="pve-input" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>Remote File Path *</label>
|
||||
<input type="text" name="scp_path" required
|
||||
<input type="text" name="scp_path" required class="pve-input"
|
||||
placeholder="/mnt/vmware/exchange-server.vmdk">
|
||||
<span class="hint">Full path to the disk image or archive on the remote server.</span>
|
||||
<span class="pve-hint">Full path to the disk image or archive on the remote server.</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<label>VM Name *</label>
|
||||
<input type="text" id="scp-vm-name" name="vm_name" required
|
||||
<input type="text" id="scp-vm-name" name="vm_name" required class="pve-input"
|
||||
placeholder="e.g. exchange-prod">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="pve-form-group">
|
||||
<label>VM ID *</label>
|
||||
<input type="number" id="scp-vmid" name="vmid" required
|
||||
<input type="number" id="scp-vmid" name="vmid" required class="pve-input"
|
||||
placeholder="e.g. 21050" min="21000" max="21100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="scp-start-btn">Start SCP Pull</button>
|
||||
<button type="submit" id="scp-start-btn" class="pve-btn pve-btn-primary">Start SCP Pull</button>
|
||||
</form>
|
||||
|
||||
<!-- Progress area -->
|
||||
<div id="scp-status" class="hidden" style="margin-top:1rem;">
|
||||
<div id="scp-status" class="pve-hidden" style="margin-top:1rem;">
|
||||
<div id="scp-phase-label" style="margin-bottom:0.5rem;"></div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="scp-progress-fill" style="width:0%">0%</div>
|
||||
<div class="pve-progress">
|
||||
<div class="pve-progress-bar" id="scp-progress-fill" style="width:0%">0%</div>
|
||||
</div>
|
||||
<div id="scp-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analysis result (populated after pull completes) -->
|
||||
<div id="scp-analysis-section"></div>
|
||||
|
||||
<script>
|
||||
// Session ID for multi-user isolation
|
||||
const SESSION_ID = '{{ session_id }}';
|
||||
|
||||
async function startScpPull(e) {
|
||||
|
|
@ -77,39 +78,32 @@ async function startScpPull(e) {
|
|||
const analysis = document.getElementById('scp-analysis-section');
|
||||
btn.disabled = true;
|
||||
analysis.innerHTML = '';
|
||||
status.classList.remove('hidden');
|
||||
status.classList.remove('pve-hidden');
|
||||
document.getElementById('scp-error').textContent = '';
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
formData.append('session_id', SESSION_ID);
|
||||
|
||||
setScpPhase('Connecting to remote server...', 0, true);
|
||||
|
||||
try {
|
||||
const resp = await fetch('/scp/start', { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.phase === 'error') {
|
||||
setScpPhase('SCP pull failed', 0, false);
|
||||
showScpError(data.error);
|
||||
btn.disabled = false;
|
||||
return;
|
||||
btn.disabled = false; return;
|
||||
}
|
||||
|
||||
// Poll progress
|
||||
const filename = data.filename;
|
||||
setScpPhase('Pulling file from remote server...', 0, true);
|
||||
|
||||
for (;;) {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
try {
|
||||
const pr = await fetch('/scp/progress/' + SESSION_ID + '/' + encodeURIComponent(filename));
|
||||
const pdata = await pr.json();
|
||||
|
||||
if (pdata.phase === 'complete') {
|
||||
setScpPhase('Pull complete (' + (pdata.file_size_gb || 0) + ' GiB)', 100, false);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
// Run analysis
|
||||
setScpPhase('Step 2/2: Analysing source image...', 0, true);
|
||||
const fd = new FormData();
|
||||
fd.append('vmid', document.getElementById('scp-vmid').value);
|
||||
|
|
@ -118,18 +112,13 @@ async function startScpPull(e) {
|
|||
fd.append('session_id', SESSION_ID);
|
||||
const ar = await fetch('/session/analyze', { method: 'POST', body: fd });
|
||||
document.getElementById('scp-analysis-section').innerHTML = await ar.text();
|
||||
document.getElementById('scp-status').classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
document.getElementById('scp-status').classList.add('pve-hidden');
|
||||
btn.disabled = false; return;
|
||||
}
|
||||
|
||||
if (pdata.phase === 'error') {
|
||||
setScpPhase('SCP pull failed', 0, false);
|
||||
showScpError(pdata.error);
|
||||
btn.disabled = false;
|
||||
return;
|
||||
showScpError(pdata.error); btn.disabled = false; return;
|
||||
}
|
||||
|
||||
if (pdata.phase === 'pulling') {
|
||||
const gb = (pdata.file_size_gb || 0).toFixed(1);
|
||||
let label = 'Pulling file... ' + gb + ' GiB';
|
||||
|
|
@ -147,14 +136,14 @@ async function startScpPull(e) {
|
|||
|
||||
function setScpPhase(label, pct, spinner) {
|
||||
const el = document.getElementById('scp-phase-label');
|
||||
if (el) el.innerHTML = (spinner ? '<span class="spinner"></span> ' : '') + label;
|
||||
if (el) el.innerHTML = (spinner ? '<span class="pve-spinner"></span> ' : '') + label;
|
||||
const fill = document.getElementById('scp-progress-fill');
|
||||
if (fill) { fill.style.width = pct + '%'; fill.textContent = pct > 0 ? pct + '%' : ''; }
|
||||
}
|
||||
|
||||
function showScpError(msg) {
|
||||
const el = document.getElementById('scp-error');
|
||||
if (el) el.innerHTML += '<div class="error" style="margin-top:0.5rem;">' + msg + '</div>';
|
||||
if (el) el.innerHTML += '<div class="pve-alert pve-alert-error" style="margin-top:0.5rem;">' + msg + '</div>';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue