vSphere-Backup-Manager/templates/create_job.html

817 lines
42 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" %}
{% 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(auto-fill, minmax(160px, 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-group">
<label class="form-label" for="replication_dest">Replication target path (optional NFS/local)</label>
<input id="replication_dest" class="form-control" type="text" name="replication_dest"
placeholder="e.g. /mnt/nfs-backup-replica" />
<div id="repNfsTargets" style="margin-top:10px; display:none;">
<div class="form-label" style="margin-bottom:6px; font-size: 11px;">Quick-Select Replication Target</div>
<div id="repNfsMountList" style="display:flex; gap:8px; flex-wrap:wrap;"></div>
</div>
</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
<span class="strategy-badge" style="background: linear-gradient(135deg, #10b981, #059669);">Recommended</span>
</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)
</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>8099% 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>
<label class="schedule-opt" id="opt-3_monthly" onclick="selectSchedule('3_monthly')">
<input type="radio" name="schedule_type" value="3_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">3 Monthly</div>
<div class="schedule-opt-desc">Every 3 months</div>
</div>
</label>
<label class="schedule-opt" id="opt-6_monthly" onclick="selectSchedule('6_monthly')">
<input type="radio" name="schedule_type" value="6_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">6 Monthly</div>
<div class="schedule-opt-desc">Every 6 months</div>
</div>
</label>
<label class="schedule-opt" id="opt-yearly" onclick="selectSchedule('yearly')">
<input type="radio" name="schedule_type" value="yearly" />
<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"/><circle cx="12" cy="14" r="2"/></svg>
</div>
<div class="schedule-opt-title">Yearly</div>
<div class="schedule-opt-desc">Once a year</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 &middot; 12 = twice daily &middot; 168 = weekly
</div>
</div>
</div>
</div>
<!-- Monthly detail -->
<div class="schedule-detail" id="detail-monthly">
<div class="form-row" id="yearly_month_row" style="display:none; margin-bottom: 12px;">
<div class="form-group" style="margin:0; grid-column: span 2;">
<label class="form-label" for="yearly_month">Month of Year</label>
<select id="yearly_month" name="yearly_month" class="form-control">
{% for m in range(1, 13) %}
<option value="{{ m }}" {% if current_month == m %}selected{% endif %}>
{{ ['January','February','March','April','May','June','July','August','September','October','November','December'][m-1] }}
</option>
{% endfor %}
</select>
</div>
</div>
<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 (128)</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) {
const allTypes = ['now','daily','weekly','monthly','3_monthly','6_monthly','yearly','interval'];
allTypes.forEach(t => {
const opt = document.getElementById('opt-' + t);
if (opt) opt.classList.remove('selected');
const d = document.getElementById('detail-' + t);
if (d) d.classList.remove('visible');
});
const selectedOpt = document.getElementById('opt-' + type);
if (selectedOpt) {
selectedOpt.classList.add('selected');
selectedOpt.querySelector('input').checked = true;
}
let detailId = 'detail-' + type;
if (['3_monthly', '6_monthly', 'yearly'].includes(type)) {
detailId = 'detail-monthly';
const yearlyRow = document.getElementById('yearly_month_row');
if (yearlyRow) {
yearlyRow.style.display = (type === 'yearly') ? '' : 'none';
}
} else {
const yearlyRow = document.getElementById('yearly_month_row');
if (yearlyRow) yearlyRow.style.display = 'none';
}
const detail = document.getElementById(detailId);
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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');
const repWrap = document.getElementById('repNfsTargets');
const repList = document.getElementById('repNfsMountList');
mounts.forEach(m => {
// Destination Button
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);
// Replication Button
const repBtn = document.createElement('button');
repBtn.type = 'button';
repBtn.className = 'btn btn-secondary btn-sm';
repBtn.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>`;
repBtn.onclick = () => {
document.getElementById('replication_dest').value = m.mountpoint;
repList.querySelectorAll('button').forEach(b => b.style.borderColor = '');
repBtn.style.borderColor = 'var(--accent)';
};
repList.appendChild(repBtn);
});
wrap.style.display = '';
if (repWrap) repWrap.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 %}