vSphere-Backup-Manager/templates/batch_job.html

752 lines
40 KiB
HTML
Raw Permalink 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 = 'vms' %}
{% block title %}Batch Backup — vSphere Backup Manager{% endblock %}
{% block head %}
<style>
.batch-wrap { max-width: 760px; }
.vm-list-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);
}
.vm-list-header {
padding: 14px 22px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
background: rgba(255,255,255,0.01);
font-size: 14.5px; font-weight: 700;
}
.vm-list-item {
display: flex; align-items: center; gap: 14px;
padding: 13px 22px;
border-bottom: 1px solid var(--border);
}
.vm-list-item:last-child { border-bottom: none; }
.vm-list-num {
width: 22px; height: 22px; border-radius: 50%;
background: rgba(99,102,241,0.12);
border: 1px solid rgba(99,102,241,0.25);
font-size: 11px; font-weight: 700; color: var(--accent);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.vm-list-name { font-weight: 600; font-size: 14px; flex: 1; }
.vm-list-state { font-size: 12px; color: var(--text-muted); }
.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: 14px 22px;
border-bottom: 1px solid var(--border);
background: rgba(255,255,255,0.01);
display: flex; align-items: center; gap: 10px;
font-size: 14.5px; font-weight: 700;
}
.section-card-body { padding: 22px; }
.disk-strategy-grid {
display: grid; grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
}
.strategy-opt {
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px 16px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
background: rgba(255,255,255,0.01);
}
.strategy-opt:hover { border-color: var(--border-bright); 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.08); }
.strategy-opt input[type=radio] { display: none; }
.strategy-icon { font-size: 24px; margin-bottom: 8px; }
.strategy-title { font-size: 13px; font-weight: 700; }
.strategy-desc { font-size: 11.5px; color: var(--text-muted); margin-top: 3px; }
.schedule-options {
display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 10px; margin-bottom: 16px;
}
.schedule-opt {
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex; align-items: flex-start; gap: 10px;
background: rgba(255,255,255,0.01);
}
.schedule-opt:hover { border-color: var(--border-bright); transform: translateY(-1px); }
.schedule-opt.selected { border-color: var(--accent); background: rgba(99,102,241,0.08); }
.schedule-opt input[type=radio] { display: none; }
.schedule-opt-icon { height: 22px; display: flex; align-items: center; }
.schedule-opt-title { font-size: 13.5px; font-weight: 700; }
.schedule-opt-desc { font-size: 12px; color: var(--text-muted); margin-top: 3px; }
.schedule-detail { display: none; margin-top: 14px; }
.schedule-detail.visible { display: block; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.action-bar { display: flex; gap: 12px; align-items: center; padding: 20px 0 40px; }
.jobs-preview {
background: rgba(99,102,241,0.05);
border: 1px solid rgba(99,102,241,0.15);
border-radius: var(--radius-sm);
padding: 12px 16px;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 20px;
}
.bkp-strategy-options {
display: grid; grid-template-columns: 1fr 1fr;
gap: 12px; margin-bottom: 4px;
}
.bkp-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);
}
.bkp-strategy-opt:hover { border-color: var(--border-bright); background: var(--bg-card-hover); transform: translateY(-1px); }
.bkp-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); }
.bkp-strategy-opt input[type=radio] { display: none; }
.bkp-strategy-opt-icon { font-size: 26px; flex-shrink: 0; margin-top: 2px; }
.bkp-strategy-opt-title { font-size: 14.5px; font-weight: 700; display: flex; align-items: center; gap: 7px; }
.bkp-strategy-opt-desc { font-size: 12px; color: var(--text-muted); margin-top: 4px; font-weight: 500; line-height: 1.5; }
.bkp-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-info-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-info-banner.visible { display: block; }
.cbt-info-banner strong { color: var(--text-primary); }
</style>
{% endblock %}
{% block content %}
<div class="topbar">
<div>
<div class="topbar-title">Batch Backup</div>
<div class="topbar-subtitle">Configure and launch backups for {{ vm_names|length }} VMs simultaneously</div>
</div>
<div class="topbar-actions">
<a href="/vms" class="btn btn-ghost btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
Back to VMs
</a>
</div>
</div>
<div class="content">
<div class="batch-wrap">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }}">{{ msg }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Preview info banner -->
<div class="jobs-preview">
<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);"><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>
This will create a <strong>single grouped backup job</strong> for all <strong>{{ vm_names|length }} selected VMs</strong>. The backups will execute sequentially to prevent datastore I/O congestion, updating a single progress bar and log.
</div>
<!-- VM list -->
<div class="vm-list-card">
<div class="vm-list-header">
<div>
<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="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>
Selected VMs ({{ vm_names|length }})
</div>
<a href="/vms" style="font-size:12px;color:var(--text-muted);text-decoration:none;">Change selection</a>
</div>
{% for name in vm_names %}
{% set vm = vms_by_name.get(name, {}) %}
<div class="vm-list-item">
<div class="vm-list-num">{{ loop.index }}</div>
<div style="flex:1;">
<div class="vm-list-name">{{ name }}</div>
<div class="vm-list-state">{{ vm.get('guest_os','') or '' }}</div>
</div>
{% if vm.get('power_state') == 'poweredOn' %}
<span class="badge badge-green">On</span>
{% elif vm.get('power_state') == 'poweredOff' %}
<span class="badge badge-red">Off</span>
{% else %}
<span class="badge badge-gray">{{ vm.get('power_state','—') }}</span>
{% endif %}
{% if vm.get('disks') %}
<span style="font-size:12px;color:var(--text-muted);">{{ vm.disks|length }} disk{{ 's' if vm.disks|length != 1 }}</span>
{% endif %}
</div>
{% endfor %}
</div>
<form method="post" action="/jobs/batch" id="batchForm">
<!-- Pass VM names through form -->
{% for name in vm_names %}
<input type="hidden" name="vms" value="{{ name }}" />
{% endfor %}
<!-- Destination -->
<div class="section-card">
<div class="section-card-header">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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="13" height="13" 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;"><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)
</div>
<div id="nfsMountList" style="display:flex; gap:8px; flex-wrap:wrap;"></div>
</div>
<div class="form-group">
<label class="form-label" for="dest">Base 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 style="font-size:12px;color:var(--text-muted);margin-top:6px;">
Each VM will be backed up into its own subfolder: <code style="font-family:'JetBrains Mono',monospace;">dest/VM_NAME/</code>
</div>
</div>
<div class="form-check" style="margin-bottom:15px;">
<input type="checkbox" id="enable_replication" name="enable_replication" onchange="toggleReplication()" />
<label for="enable_replication">Enable replication to secondary target (NFS/local)</label>
</div>
<div class="form-group" id="replication_section" style="display:none;">
<label class="form-label" for="replication_dest">Replication target path</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 style="font-size:12px;color:var(--text-muted);margin-top:6px;">
Successful VM backups will be replicated sequentially to: <code style="font-family:'JetBrains Mono',monospace;">replication_dest/VM_NAME/</code>
</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>
</div>
</div>
<!-- Disk Strategy -->
<div class="section-card">
<div class="section-card-header">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
Disk Strategy
</div>
<div class="section-card-body">
<div class="disk-strategy-grid">
<label class="strategy-opt selected" id="strat-all" onclick="selectStrategy('all')">
<input type="radio" name="disk_strategy" value="all" checked />
<div class="strategy-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="2" y="2" width="20" height="20" rx="2"/><path d="M8 6h8"/><path d="M8 10h8"/><path d="M8 14h4"/></svg>
</div>
<div class="strategy-title">All Disks</div>
<div class="strategy-desc">Back up every VMDK attached</div>
</label>
<label class="strategy-opt" id="strat-os" onclick="selectStrategy('os')">
<input type="radio" name="disk_strategy" value="os" />
<div class="strategy-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--success);"><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>
</div>
<div class="strategy-title">OS Disk Only</div>
<div class="strategy-desc">Smallest disk per VM<br><span style="color:var(--success);font-weight:600;">Best for ipcam VMs</span></div>
</label>
<label class="strategy-opt" id="strat-vmx" onclick="selectStrategy('vmx')">
<input type="radio" name="disk_strategy" value="vmx" />
<div class="strategy-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--text-muted);"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
</div>
<div class="strategy-title">Config Only</div>
<div class="strategy-desc">VMX file only, no disk data</div>
</label>
</div>
<div style="margin-top:12px;padding:10px 14px;background:rgba(6,182,212,0.06);border:1px solid rgba(6,182,212,0.15);border-radius:var(--radius-sm);font-size:12.5px;color:var(--text-secondary);" id="stratHint">
All VMDK disks will be backed up for each VM.
</div>
</div>
</div>
<!-- Backup Strategy (CBT) -->
<div class="section-card">
<div class="section-card-header">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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="bkp-strategy-options">
<label class="bkp-strategy-opt selected" id="bkp-full" onclick="selectBkpStrategy('full')">
<input type="radio" name="use_cbt" value="" checked id="bkp_radio_full" />
<div class="bkp-strategy-opt-icon">💾</div>
<div>
<div class="bkp-strategy-opt-title">
Full Backup
<span class="bkp-strategy-badge" style="background: linear-gradient(135deg, #10b981, #059669);">Recommended</span>
</div>
<div class="bkp-strategy-opt-desc">Download the entire disk image every run. Best for initial backups or infrequent schedules.</div>
</div>
</label>
<label class="bkp-strategy-opt" id="bkp-incremental" onclick="selectBkpStrategy('incremental')">
<input type="radio" name="use_cbt" value="1" id="bkp_radio_incremental" />
<div class="bkp-strategy-opt-icon"></div>
<div>
<div class="bkp-strategy-opt-title">
Incremental (CBT)
</div>
<div class="bkp-strategy-opt-desc">Transfer only <strong>changed blocks</strong> via VMware CBT. Dramatic daily transfer reduction.</div>
</div>
</label>
</div>
<div class="cbt-info-banner" id="cbtInfoBanner">
<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 is a full backup that seeds the CBT state. All subsequent runs transfer only changed blocks.
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="15" height="15" 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="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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 selected" id="opt-now" onclick="selectSchedule('now')">
<input type="radio" name="schedule_type" value="now" checked />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" 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 immediately</div>
</div>
</label>
<label class="schedule-opt" id="opt-daily" onclick="selectSchedule('daily')">
<input type="radio" name="schedule_type" value="daily" />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" 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">Every day at 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">
<svg width="18" height="18" 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"/></svg>
</div>
<div class="schedule-opt-title">Weekly</div>
<div class="schedule-opt-desc">Specific day each week</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">
<svg width="18" height="18" 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">
<svg width="18" height="18" 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">
<svg width="18" height="18" 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">
<svg width="18" height="18" 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>
<label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')">
<input type="radio" name="schedule_type" value="interval" />
<div>
<div class="schedule-opt-icon">
<svg width="18" height="18" 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">Every N Hours</div>
<div class="schedule-opt-desc">Repeat on an interval</div>
</div>
</label>
</div>
<!-- Detail panels -->
<div class="schedule-detail" 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>
<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>
<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>
<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">Repeat every (hours)</label>
<input id="interval_hours" class="form-control" type="number"
name="interval_hours" min="1" max="8760" value="24" />
<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>
<div class="form-group" style="margin-top:16px; margin-bottom:0;">
<label class="form-label" for="job_label">Label prefix (optional)</label>
<input id="job_label" class="form-control" type="text" name="job_label"
placeholder="e.g. Nightly ipcam 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;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Launch Grouped Backup ({{ vm_names|length }} VMs)
</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>
const ALL_SCHED = ['now','daily','weekly','monthly','3_monthly','6_monthly','yearly','interval'];
function selectSchedule(type) {
ALL_SCHED.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');
}
const stratHints = {
all: 'All VMDK disks will be backed up for each VM.',
os: 'Only the smallest disk per VM (OS disk) will be backed up — ideal for ipcam/DVR VMs with large video storage.',
vmx: 'Only the VM configuration file (.vmx) will be saved — no disk data.',
};
function selectStrategy(type) {
['all','os','vmx'].forEach(t => document.getElementById('strat-' + t).classList.remove('selected'));
document.getElementById('strat-' + type).classList.add('selected');
document.getElementById('strat-' + type).querySelector('input').checked = true;
document.getElementById('stratHint').textContent = stratHints[type];
}
function selectBkpStrategy(type) {
document.getElementById('bkp-full').classList.remove('selected');
document.getElementById('bkp-incremental').classList.remove('selected');
document.getElementById('bkp-' + type).classList.add('selected');
document.getElementById('bkp_radio_' + type).checked = true;
const banner = document.getElementById('cbtInfoBanner');
if (type === 'incremental') {
banner.classList.add('visible');
} else {
banner.classList.remove('visible');
}
}
document.getElementById('batchForm').addEventListener('submit', function() {
document.getElementById('submitText').style.display = 'none';
document.getElementById('submitSpinner').style.display = 'inline-block';
document.getElementById('submitBtn').disabled = true;
});
// Load NFS mounts
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:4px;"><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:4px;"><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 = '';
}
}
function toggleReplication() {
const chk = document.getElementById('enable_replication');
const sec = document.getElementById('replication_section');
if (chk.checked) {
sec.style.display = '';
} else {
sec.style.display = 'none';
document.getElementById('replication_dest').value = '';
}
}
</script>
{% endblock %}