725 lines
36 KiB
HTML
725 lines
36 KiB
HTML
{% extends "base.html" %}
|
||
{% set active_page = 'create_job' %}
|
||
{% block title %}Create Backup Job — vSphere Backup Manager{% endblock %}
|
||
|
||
{% block head %}
|
||
<style>
|
||
.wizard-wrap { max-width: 720px; }
|
||
|
||
.wizard-steps {
|
||
display: flex; align-items: center; gap: 0;
|
||
margin-bottom: 32px; counter-reset: step;
|
||
}
|
||
.step {
|
||
display: flex; align-items: center; gap: 12px;
|
||
flex: 1; position: relative;
|
||
}
|
||
.step:not(:last-child)::after {
|
||
content: '';
|
||
flex: 1; height: 2px;
|
||
background: var(--border);
|
||
margin: 0 16px;
|
||
border-radius: 2px;
|
||
}
|
||
.step.done:not(:last-child)::after { background: var(--accent); }
|
||
.step-num {
|
||
width: 32px; height: 32px; border-radius: 50%;
|
||
border: 2px solid var(--border);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 13px; font-weight: 700; flex-shrink: 0;
|
||
color: var(--text-muted);
|
||
transition: all 0.25s ease;
|
||
}
|
||
.step.active .step-num { border-color: var(--accent); color: #ffffff; background: var(--accent); box-shadow: 0 0 12px var(--accent-glow); }
|
||
.step.done .step-num { border-color: var(--accent-2); background: var(--accent-2); color: #ffffff; box-shadow: 0 0 12px rgba(6, 182, 212, 0.25); }
|
||
.step-label { font-size: 13.5px; font-weight: 600; color: var(--text-muted); }
|
||
.step.active .step-label { color: var(--text-primary); }
|
||
|
||
.section-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
margin-bottom: 20px;
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.section-card-header {
|
||
padding: 16px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: rgba(255,255,255,0.01);
|
||
display: flex; align-items: center; gap: 12px;
|
||
font-size: 14.5px; font-weight: 700;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.section-card-body { padding: 24px; }
|
||
|
||
.schedule-options {
|
||
display: grid; grid-template-columns: repeat(2, 1fr);
|
||
gap: 12px; margin-bottom: 20px;
|
||
}
|
||
.schedule-opt {
|
||
border: 1.5px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 16px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
background: rgba(255, 255, 255, 0.01);
|
||
}
|
||
.schedule-opt:hover { border-color: var(--border-bright); background: var(--bg-card-hover); transform: translateY(-1px); }
|
||
.schedule-opt.selected { border-color: var(--accent); background: rgba(99, 102, 241, 0.08); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.05); }
|
||
.schedule-opt input[type=radio] { display: none; }
|
||
.schedule-opt-icon { font-size: 22px; }
|
||
.schedule-opt-title { font-size: 14.5px; font-weight: 700; }
|
||
.schedule-opt-desc { font-size: 12px; color: var(--text-muted); margin-top: 3px; font-weight: 500; }
|
||
|
||
.schedule-detail { display: none; }
|
||
.schedule-detail.visible { display: block; }
|
||
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||
|
||
.sftp-toggle-btn {
|
||
background: none; border: none; cursor: pointer;
|
||
color: var(--accent-2); font-size: 13.5px; font-weight: 600;
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 4px 8px; margin-top: 8px;
|
||
transition: color 0.2s;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
.sftp-toggle-btn:hover {
|
||
color: var(--accent);
|
||
background: rgba(255,255,255,0.02);
|
||
}
|
||
.sftp-section { display: none; margin-top: 20px; }
|
||
.sftp-section.visible { display: block; }
|
||
|
||
.strategy-options {
|
||
display: grid; grid-template-columns: 1fr 1fr;
|
||
gap: 12px; margin-bottom: 4px;
|
||
}
|
||
.strategy-opt {
|
||
border: 1.5px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 16px 20px;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex; align-items: flex-start; gap: 14px;
|
||
background: rgba(255, 255, 255, 0.01);
|
||
position: relative;
|
||
}
|
||
.strategy-opt:hover { border-color: var(--border-bright); background: var(--bg-card-hover); transform: translateY(-1px); }
|
||
.strategy-opt.selected { border-color: var(--accent); background: rgba(99, 102, 241, 0.08); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.05); }
|
||
.strategy-opt input[type=radio] { display: none; }
|
||
.strategy-opt-icon { font-size: 26px; flex-shrink: 0; margin-top: 2px; }
|
||
.strategy-opt-title { font-size: 14.5px; font-weight: 700; display: flex; align-items: center; gap: 7px; }
|
||
.strategy-opt-desc { font-size: 12px; color: var(--text-muted); margin-top: 4px; font-weight: 500; line-height: 1.5; }
|
||
.strategy-badge {
|
||
font-size: 10px; font-weight: 800; letter-spacing: 0.04em;
|
||
padding: 2px 7px; border-radius: 100px;
|
||
background: linear-gradient(135deg, #6366f1, #06b6d4);
|
||
color: #fff; text-transform: uppercase;
|
||
}
|
||
.cbt-savings-banner {
|
||
margin-top: 14px; padding: 12px 16px;
|
||
background: rgba(99,102,241,0.06);
|
||
border: 1px solid rgba(99,102,241,0.18);
|
||
border-radius: var(--radius-sm);
|
||
font-size: 13px; color: var(--text-secondary);
|
||
display: none;
|
||
}
|
||
.cbt-savings-banner.visible { display: block; }
|
||
.cbt-savings-banner strong { color: var(--text-primary); }
|
||
|
||
.action-bar {
|
||
display: flex; gap: 14px; align-items: center;
|
||
padding: 24px 0 0;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="topbar">
|
||
<div>
|
||
<div class="topbar-title">Create Backup Job</div>
|
||
<div class="topbar-subtitle">Configure a new backup job for a virtual machine</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="wizard-wrap">
|
||
<!-- Progress steps -->
|
||
<div class="wizard-steps">
|
||
<div class="step done">
|
||
<div class="step-num">✓</div>
|
||
<div class="step-label">Connected</div>
|
||
</div>
|
||
<div class="step active">
|
||
<div class="step-num">2</div>
|
||
<div class="step-label">Configure Job</div>
|
||
</div>
|
||
<div class="step">
|
||
<div class="step-num">3</div>
|
||
<div class="step-label">Review & Run</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form method="post" action="/jobs/create" id="jobForm">
|
||
|
||
<!-- VM Selection -->
|
||
<div class="section-card">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||
Virtual Machine
|
||
</div>
|
||
<div class="section-card-body">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="vm_name">Select VM to back up</label>
|
||
<select id="vm_name" name="vm_name" class="form-control" required
|
||
onchange="onVmChange(this.value)">
|
||
<option value="">— Choose a VM —</option>
|
||
{% for vm in vms %}
|
||
<option value="{{ vm.name }}"
|
||
{% if vm.name == selected_vm %}selected{% endif %}>
|
||
{{ vm.name }}
|
||
{% if vm.power_state == 'poweredOn' %}🟢{% elif vm.power_state == 'poweredOff' %}🔴{% else %}🟡{% endif %}
|
||
({{ vm.guest_os[:30] if vm.guest_os else 'Unknown' }})
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Disk Selection (shown after VM pick) -->
|
||
<input type="hidden" name="disk_selection_shown" id="disk_selection_shown" value="" />
|
||
<div class="section-card" id="diskCard" style="display:none;">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="2" width="20" height="20" rx="2" ry="2"/><path d="M12 18h.01"/><path d="M8 6h8"/><path d="M8 10h8"/></svg>
|
||
Select Disks to Backup
|
||
<span id="diskCardBadge" style="margin-left:auto;font-size:12px;color:var(--text-muted);font-weight:500;"></span>
|
||
</div>
|
||
<div class="section-card-body">
|
||
<div id="diskLoader" style="text-align:center;padding:24px;color:var(--text-muted);">
|
||
<span class="spinner" style="vertical-align:middle;margin-right:8px;"></span> Loading disk info…
|
||
</div>
|
||
<div id="diskList"></div>
|
||
<div id="diskTip" style="display:none;margin-top:14px;padding:12px 16px;
|
||
background:rgba(6,182,212,0.06);border:1px solid rgba(6,182,212,0.15);
|
||
border-radius:var(--radius-sm);font-size:13px;color:var(--text-secondary);">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||
<strong>Tip:</strong> Uncheck large data disks (e.g. video storage) to skip them.
|
||
The VM config (.vmx) is always included. For ipcam VMs, keep only the small OS disk checked.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Destination -->
|
||
<div class="section-card">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||
Destination
|
||
</div>
|
||
<div class="section-card-body">
|
||
<!-- NFS quick-select -->
|
||
<div id="nfsTargets" style="margin-bottom:14px; display:none;">
|
||
<div class="form-label" style="margin-bottom:6px;">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||
NFS Mounts (click to use as destination)
|
||
</div>
|
||
<div id="nfsMountList" style="display:flex; gap:8px; flex-wrap:wrap;"></div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label" for="dest">Local backup path</label>
|
||
<input id="dest" class="form-control" type="text" name="dest"
|
||
value="./backups" placeholder="e.g. /mnt/nfs-backup or /data/vmbackups" required />
|
||
</div>
|
||
<div class="form-check">
|
||
<input type="checkbox" id="compress" name="compress" />
|
||
<label for="compress">Compress with zstd (smaller files, slower)</label>
|
||
</div>
|
||
<div class="form-check">
|
||
<input type="checkbox" id="no_verify_ssl" name="no_verify_ssl"
|
||
{% if session.get('no_verify_ssl') %}checked{% endif %} />
|
||
<label for="no_verify_ssl">Skip SSL certificate verification</label>
|
||
</div>
|
||
|
||
<!-- SFTP Toggle -->
|
||
<button type="button" class="sftp-toggle-btn" onclick="toggleSFTP()">
|
||
<span id="sftpToggleIcon">▶</span>
|
||
Upload to SFTP server (optional)
|
||
</button>
|
||
<div class="sftp-section" id="sftpSection">
|
||
<div class="form-row" style="margin-top:14px;">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="sftp_host">SFTP Host</label>
|
||
<input id="sftp_host" class="form-control" type="text" name="sftp_host" placeholder="sftp.example.com" />
|
||
</div>
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="sftp_user">SFTP Username</label>
|
||
<input id="sftp_user" class="form-control" type="text" name="sftp_user" placeholder="backupuser" />
|
||
</div>
|
||
</div>
|
||
<div class="form-group" style="margin-top:12px;">
|
||
<label class="form-label" for="sftp_password">SFTP Password</label>
|
||
<input id="sftp_password" class="form-control" type="password" name="sftp_password" placeholder="••••••••" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Backup Strategy (CBT) -->
|
||
<div class="section-card">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||
Backup Strategy
|
||
</div>
|
||
<div class="section-card-body">
|
||
<div class="strategy-options">
|
||
|
||
<label class="strategy-opt selected" id="strat-full" onclick="selectStrategy('full')">
|
||
<input type="radio" name="use_cbt" value="" checked id="strat_radio_full" />
|
||
<div class="strategy-opt-icon">💾</div>
|
||
<div>
|
||
<div class="strategy-opt-title">Full Backup</div>
|
||
<div class="strategy-opt-desc">Download the entire disk image every run. Simple and always complete — best for first-time setups or infrequent backups.</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="strategy-opt" id="strat-incremental" onclick="selectStrategy('incremental')">
|
||
<input type="radio" name="use_cbt" value="1" id="strat_radio_incremental" />
|
||
<div class="strategy-opt-icon">⚡</div>
|
||
<div>
|
||
<div class="strategy-opt-title">
|
||
Incremental (CBT)
|
||
<span class="strategy-badge">⭐ Enterprise</span>
|
||
</div>
|
||
<div class="strategy-opt-desc">Transfer only <strong>changed blocks</strong> using VMware Changed Block Tracking. Dramatically reduces daily backup size.</div>
|
||
</div>
|
||
</label>
|
||
|
||
</div>
|
||
|
||
<div class="cbt-savings-banner" id="cbtBanner">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:6px;color:var(--accent);"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||
<strong>80–99% less data transferred on daily runs.</strong>
|
||
The first run performs a full backup to seed the CBT state. All subsequent runs download only the blocks that changed since the last backup.
|
||
CBT requires ESXi 4.0+ and VM hardware version 7+.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Retention Policy -->
|
||
<div class="section-card">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||
Retention Policy
|
||
</div>
|
||
<div class="section-card-body">
|
||
<div class="form-row">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="retention_type">Backups to keep</label>
|
||
<select id="retention_type" name="retention_type" class="form-control" onchange="onRetentionChange(this.value)">
|
||
<option value="keep_all">Keep all backups (No automatic deletion)</option>
|
||
<option value="keep_count">Keep latest N backups (Count based)</option>
|
||
<option value="keep_days">Keep backups for N days (Age based)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="retention_val_group" style="margin:0; display:none;">
|
||
<label class="form-label" for="retention_value">Number of backups (N)</label>
|
||
<input id="retention_value" class="form-control" type="number" name="retention_value" value="5" min="1" max="1000" />
|
||
</div>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--text-muted);margin-top:10px;" id="retention_hint">
|
||
All successful backup copies will be preserved indefinitely.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Schedule -->
|
||
<div class="section-card">
|
||
<div class="section-card-header">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||
Schedule
|
||
</div>
|
||
<div class="section-card-body">
|
||
<div class="schedule-options">
|
||
|
||
<label class="schedule-opt {% if not show_schedule %}selected{% endif %}" id="opt-now" onclick="selectSchedule('now')">
|
||
<input type="radio" name="schedule_type" value="now" {% if not show_schedule %}checked{% endif %} />
|
||
<div>
|
||
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||
</div>
|
||
<div class="schedule-opt-title">Run Now</div>
|
||
<div class="schedule-opt-desc">Start the backup immediately</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="schedule-opt {% if show_schedule %}selected{% endif %}" id="opt-daily" onclick="selectSchedule('daily')">
|
||
<input type="radio" name="schedule_type" value="daily" {% if show_schedule %}checked{% endif %}/>
|
||
<div>
|
||
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||
</div>
|
||
<div class="schedule-opt-title">Daily</div>
|
||
<div class="schedule-opt-desc">Repeat every day at a set time</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="schedule-opt" id="opt-weekly" onclick="selectSchedule('weekly')">
|
||
<input type="radio" name="schedule_type" value="weekly" />
|
||
<div>
|
||
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>
|
||
</div>
|
||
<div class="schedule-opt-title">Weekly</div>
|
||
<div class="schedule-opt-desc">Repeat every week on a specific day</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')">
|
||
<input type="radio" name="schedule_type" value="interval" />
|
||
<div>
|
||
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||
</div>
|
||
<div class="schedule-opt-title">Interval</div>
|
||
<div class="schedule-opt-desc">Repeat every N hours</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="schedule-opt" id="opt-monthly" onclick="selectSchedule('monthly')">
|
||
<input type="radio" name="schedule_type" value="monthly" />
|
||
<div>
|
||
<div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h4"/><path d="M8 18h2"/></svg>
|
||
</div>
|
||
<div class="schedule-opt-title">Monthly</div>
|
||
<div class="schedule-opt-desc">Specific day each month</div>
|
||
</div>
|
||
</label>
|
||
|
||
</div>
|
||
|
||
<!-- Daily detail -->
|
||
<div class="schedule-detail {% if show_schedule %}visible{% endif %}" id="detail-daily">
|
||
<div class="form-row">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="daily_time">Time (24h)</label>
|
||
<input id="daily_time" class="form-control" type="time" name="daily_time" value="02:00" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Weekly detail -->
|
||
<div class="schedule-detail" id="detail-weekly">
|
||
<div class="form-row">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="weekly_day">Day of Week</label>
|
||
<select id="weekly_day" class="form-control" name="weekly_day">
|
||
<option value="0">Monday</option>
|
||
<option value="1">Tuesday</option>
|
||
<option value="2">Wednesday</option>
|
||
<option value="3">Thursday</option>
|
||
<option value="4">Friday</option>
|
||
<option value="5">Saturday</option>
|
||
<option value="6">Sunday</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="weekly_time">Time (24h)</label>
|
||
<input id="weekly_time" class="form-control" type="time" name="weekly_time" value="02:00" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Interval detail -->
|
||
<div class="schedule-detail" id="detail-interval">
|
||
<div class="form-row">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="interval_hours">Every (hours)</label>
|
||
<input id="interval_hours" class="form-control" type="number"
|
||
name="interval_hours" value="24" min="1" max="8760" />
|
||
<div style="font-size:12px;color:var(--text-muted);margin-top:4px;">
|
||
e.g. 6 = every 6h · 12 = twice daily · 168 = weekly
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Monthly detail -->
|
||
<div class="schedule-detail" id="detail-monthly">
|
||
<div class="form-row" style="margin-bottom: 12px;">
|
||
<div class="form-group" style="margin:0; grid-column: span 2;">
|
||
<label class="form-label" for="monthly_basis">Monthly Schedule Type</label>
|
||
<select id="monthly_basis" name="monthly_basis" class="form-control" onchange="onMonthlyBasisChange(this.value)">
|
||
<option value="day_num">Specific Day of Month (e.g. 1st, 15th)</option>
|
||
<option value="weekday">Specific Weekday of Month (e.g. 1st Sunday, Last Saturday)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row" id="monthly_day_num_row">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="monthly_day">Day of month (1–28)</label>
|
||
<input id="monthly_day" class="form-control" type="number"
|
||
name="monthly_day" min="1" max="28" value="1" />
|
||
</div>
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="monthly_time_1">Time (24h)</label>
|
||
<input id="monthly_time_1" class="form-control" type="time" name="monthly_time_1" value="02:00" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row" id="monthly_weekday_row" style="display:none;">
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="monthly_week_num">Which Week</label>
|
||
<select id="monthly_week_num" name="monthly_week_num" class="form-control">
|
||
<option value="1st">1st (First)</option>
|
||
<option value="2nd">2nd (Second)</option>
|
||
<option value="3rd">3rd (Third)</option>
|
||
<option value="4th">4th (Fourth)</option>
|
||
<option value="last">Last</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin:0;">
|
||
<label class="form-label" for="monthly_day_of_week">Day of Week</label>
|
||
<select id="monthly_day_of_week" name="monthly_day_of_week" class="form-control">
|
||
<option value="sun" selected>Sunday (Weekend)</option>
|
||
<option value="sat">Saturday (Weekend)</option>
|
||
<option value="mon">Monday</option>
|
||
<option value="tue">Tuesday</option>
|
||
<option value="wed">Wednesday</option>
|
||
<option value="thu">Thursday</option>
|
||
<option value="fri">Friday</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin:0; grid-column: span 2; margin-top: 12px;">
|
||
<label class="form-label" for="monthly_time_2">Time (24h)</label>
|
||
<input id="monthly_time_2" class="form-control" type="time" name="monthly_time_2" value="02:00" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Job label -->
|
||
<div class="form-group" style="margin-top: 18px; margin-bottom:0">
|
||
<label class="form-label" for="job_label">Job label (optional)</label>
|
||
<input id="job_label" class="form-control" type="text" name="job_label"
|
||
placeholder="e.g. Nightly web-server backup" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-bar">
|
||
<button type="submit" id="submitBtn" class="btn btn-primary">
|
||
<span id="submitText">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;"><polyline points="20 6 9 17 4 12"/></svg>
|
||
Create Job
|
||
</span>
|
||
<span id="submitSpinner" class="spinner" style="display:none;"></span>
|
||
</button>
|
||
<a href="/vms" class="btn btn-ghost">Cancel</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function selectSchedule(type) {
|
||
['now','daily','weekly','monthly','interval'].forEach(t => {
|
||
document.getElementById('opt-' + t).classList.remove('selected');
|
||
const d = document.getElementById('detail-' + t);
|
||
if (d) d.classList.remove('visible');
|
||
});
|
||
document.getElementById('opt-' + type).classList.add('selected');
|
||
document.getElementById('opt-' + type).querySelector('input').checked = true;
|
||
const detail = document.getElementById('detail-' + type);
|
||
if (detail) detail.classList.add('visible');
|
||
}
|
||
|
||
function selectStrategy(type) {
|
||
document.getElementById('strat-full').classList.remove('selected');
|
||
document.getElementById('strat-incremental').classList.remove('selected');
|
||
document.getElementById('strat-' + type).classList.add('selected');
|
||
document.getElementById('strat_radio_' + type).checked = true;
|
||
const banner = document.getElementById('cbtBanner');
|
||
if (type === 'incremental') {
|
||
banner.classList.add('visible');
|
||
} else {
|
||
banner.classList.remove('visible');
|
||
}
|
||
}
|
||
|
||
function toggleSFTP() {
|
||
const sec = document.getElementById('sftpSection');
|
||
const ico = document.getElementById('sftpToggleIcon');
|
||
sec.classList.toggle('visible');
|
||
ico.textContent = sec.classList.contains('visible') ? '▼' : '▶';
|
||
}
|
||
|
||
document.getElementById('jobForm').addEventListener('submit', function() {
|
||
document.getElementById('submitText').textContent = 'Starting…';
|
||
document.getElementById('submitSpinner').style.display = 'inline-block';
|
||
document.getElementById('submitBtn').disabled = true;
|
||
});
|
||
|
||
{% if show_schedule %}
|
||
selectSchedule('daily');
|
||
{% endif %}
|
||
|
||
// Pre-fill dest from ?dest= query param
|
||
const urlDest = new URLSearchParams(window.location.search).get('dest');
|
||
if (urlDest) document.getElementById('dest').value = urlDest;
|
||
|
||
// ── Disk Selection ──────────────────────────────────────────────────────────
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
function onVmChange(vmName) {
|
||
const diskCard = document.getElementById('diskCard');
|
||
if (!vmName) {
|
||
diskCard.style.display = 'none';
|
||
document.getElementById('disk_selection_shown').value = '';
|
||
return;
|
||
}
|
||
diskCard.style.display = '';
|
||
document.getElementById('disk_selection_shown').value = '1';
|
||
document.getElementById('diskLoader').style.display = 'block';
|
||
document.getElementById('diskList').innerHTML = '';
|
||
document.getElementById('diskTip').style.display = 'none';
|
||
document.getElementById('diskCardBadge').textContent = '';
|
||
|
||
fetch('/api/vm/' + encodeURIComponent(vmName) + '/disks')
|
||
.then(r => r.json())
|
||
.then(disks => {
|
||
document.getElementById('diskLoader').style.display = 'none';
|
||
if (!Array.isArray(disks) || !disks.length) {
|
||
document.getElementById('diskList').innerHTML =
|
||
'<div style="color:var(--text-muted);font-size:13px;">No virtual disks found on this VM.</div>';
|
||
return;
|
||
}
|
||
|
||
// Sort smallest first (OS disk is usually smallest)
|
||
disks.sort((a, b) => a.size_gb - b.size_gb);
|
||
|
||
let html = '';
|
||
disks.forEach((disk, i) => {
|
||
const sizeLabel = disk.size_gb >= 1000
|
||
? (disk.size_gb/1024).toFixed(1) + ' TB'
|
||
: disk.size_gb + ' GB';
|
||
const sizeColor = disk.size_gb > 100 ? 'var(--warning)' : 'var(--success)';
|
||
const hint = i === 0
|
||
? ' <span style="font-size:10px;color:var(--success);font-weight:700;margin-left:4px;">OS</span>'
|
||
: '';
|
||
html += `<div style="display:flex;align-items:center;gap:12px;padding:12px 0;
|
||
border-bottom:${i < disks.length-1 ? '1px solid var(--border)' : 'none'}">
|
||
<input type="checkbox" id="disk_${i}" name="disk_filter"
|
||
value="${escHtml(disk.path)}" checked
|
||
style="width:18px;height:18px;accent-color:var(--accent);flex-shrink:0;cursor:pointer;" />
|
||
<label for="disk_${i}" style="flex:1;cursor:pointer;">
|
||
<div style="font-weight:600;font-size:13.5px;">${escHtml(disk.label)}${hint}</div>
|
||
<div style="font-family:'JetBrains Mono',monospace;font-size:11.5px;
|
||
color:var(--text-muted);margin-top:3px;">${escHtml(disk.path)}</div>
|
||
</label>
|
||
<span style="background:rgba(255,255,255,0.04);border:1px solid var(--border);
|
||
padding:4px 10px;border-radius:100px;font-size:12px;
|
||
font-weight:700;color:${sizeColor};flex-shrink:0;">${sizeLabel}</span>
|
||
</div>`;
|
||
});
|
||
html += `<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;">
|
||
<button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(true)">All</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(false)">None</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" onclick="selectOsOnly()"
|
||
title="Select only the smallest disk (usually the OS disk)">OS Only</button>
|
||
</div>`;
|
||
|
||
document.getElementById('diskList').innerHTML = html;
|
||
document.getElementById('diskTip').style.display = '';
|
||
document.getElementById('diskCardBadge').textContent =
|
||
disks.length + ' disk' + (disks.length > 1 ? 's' : '') + ' found';
|
||
})
|
||
.catch(() => {
|
||
document.getElementById('diskLoader').style.display = 'none';
|
||
document.getElementById('diskList').innerHTML =
|
||
'<div style="color:var(--text-muted);font-size:13px;">Failed to load disk list</div>';
|
||
});
|
||
}
|
||
|
||
function selectAllDisks(checked) {
|
||
document.querySelectorAll('input[name="disk_filter"]').forEach(cb => cb.checked = checked);
|
||
}
|
||
function selectOsOnly() {
|
||
// Smallest disk is first (sorted above)
|
||
const cbs = document.querySelectorAll('input[name="disk_filter"]');
|
||
cbs.forEach((cb, i) => { cb.checked = (i === 0); });
|
||
}
|
||
|
||
// Auto-trigger disk load if VM pre-selected (from ?vm=)
|
||
const initVm = document.getElementById('vm_name').value;
|
||
if (initVm) onVmChange(initVm);
|
||
|
||
// Load NFS mounts for quick-select
|
||
fetch('/api/nfs')
|
||
.then(r => r.json())
|
||
.then(mounts => {
|
||
if (!mounts || !mounts.length) return;
|
||
const wrap = document.getElementById('nfsTargets');
|
||
const list = document.getElementById('nfsMountList');
|
||
mounts.forEach(m => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'btn btn-secondary btn-sm';
|
||
btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> ${m.mountpoint} <span style="color:var(--text-muted);font-size:11px;margin-left:4px;">${m.free_gb}GB free</span>`;
|
||
btn.onclick = () => {
|
||
document.getElementById('dest').value = m.mountpoint;
|
||
list.querySelectorAll('button').forEach(b => b.style.borderColor = '');
|
||
btn.style.borderColor = 'var(--accent)';
|
||
};
|
||
list.appendChild(btn);
|
||
});
|
||
wrap.style.display = '';
|
||
})
|
||
.catch(() => {});
|
||
|
||
function onRetentionChange(val) {
|
||
const valGroup = document.getElementById('retention_val_group');
|
||
const hint = document.getElementById('retention_hint');
|
||
const input = document.getElementById('retention_value');
|
||
|
||
if (val === 'keep_all') {
|
||
valGroup.style.display = 'none';
|
||
hint.textContent = 'All successful backup copies will be preserved indefinitely.';
|
||
} else if (val === 'keep_count') {
|
||
valGroup.style.display = '';
|
||
input.min = '1';
|
||
input.value = input.value || '5';
|
||
document.querySelector('label[for="retention_value"]').textContent = 'Number of backups (N)';
|
||
hint.textContent = 'Only the latest N successful backups will be kept. Older ones will be deleted automatically.';
|
||
} else if (val === 'keep_days') {
|
||
valGroup.style.display = '';
|
||
input.min = '1';
|
||
input.value = input.value || '7';
|
||
document.querySelector('label[for="retention_value"]').textContent = 'Number of days (N)';
|
||
hint.textContent = 'Backups older than N days will be deleted automatically.';
|
||
}
|
||
}
|
||
|
||
function onMonthlyBasisChange(val) {
|
||
const dayNumRow = document.getElementById('monthly_day_num_row');
|
||
const weekdayRow = document.getElementById('monthly_weekday_row');
|
||
if (val === 'day_num') {
|
||
dayNumRow.style.display = '';
|
||
weekdayRow.style.display = 'none';
|
||
} else {
|
||
dayNumRow.style.display = 'none';
|
||
weekdayRow.style.display = '';
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|