Sync from main server - 2026-05-13 01:06:32
This commit is contained in:
@@ -10,24 +10,54 @@
|
||||
<div class="form-section-title">STEP 1 — SELECT BACKUP SOURCE</div>
|
||||
<div class="radio-group">
|
||||
<label class="radio-card">
|
||||
<input type="radio" name="backup_source" value="local" checked onchange="updateBackupList()">
|
||||
<input type="radio" name="backup_source" value="local" checked onchange="nvUpdateSource()">
|
||||
<div class="radio-body"><span class="radio-icon">🖥️</span><div><div class="radio-label">Main Server</div><div class="radio-desc"><code>/root/backups/</code></div></div></div>
|
||||
</label>
|
||||
<label class="radio-card">
|
||||
<input type="radio" name="backup_source" value="vm" onchange="updateBackupList()">
|
||||
<input type="radio" name="backup_source" value="vm" onchange="nvUpdateSource()">
|
||||
<div class="radio-body"><span class="radio-icon">💾</span><div><div class="radio-label">VM Backup Server</div><div class="radio-desc"><code>/backups/main-server/</code></div></div></div>
|
||||
</label>
|
||||
<label class="radio-card">
|
||||
<input type="radio" name="backup_source" value="cloud" onchange="nvUpdateSource()">
|
||||
<div class="radio-body"><span class="radio-icon">☁️</span><div><div class="radio-label">Cloudflare R2</div><div class="radio-desc">cloud storage</div></div></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{# Separate selects — only one shown at a time #}
|
||||
<div class="form-group" style="margin-top:14px; max-width:500px;">
|
||||
<label class="form-label">BACKUP FILE</label>
|
||||
<select id="backup-file-select" class="form-input">
|
||||
<optgroup label="Main Server" id="local-options">
|
||||
{% for b in backups %}<option value="{{ b }}" data-source="local">{{ b }}</option>{% else %}<option disabled>No local backups</option>{% endfor %}
|
||||
</optgroup>
|
||||
<optgroup label="VM Backups" id="vm-options" style="display:none;">
|
||||
{% for b in vm_backups %}<option value="{{ b }}" data-source="vm">{{ b }}</option>{% else %}<option disabled>No VM backups</option>{% endfor %}
|
||||
</optgroup>
|
||||
|
||||
{# Local select #}
|
||||
<select id="select-local" class="form-input">
|
||||
{% for b in backups %}
|
||||
<option value="{{ b }}">{{ b }}</option>
|
||||
{% else %}
|
||||
<option disabled>No local backups</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{# VM select #}
|
||||
<select id="select-vm" class="form-input" style="display:none;">
|
||||
{% for b in vm_backups %}
|
||||
<option value="{{ b }}">{{ b }}</option>
|
||||
{% else %}
|
||||
<option disabled>No VM backups</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{# Cloud select — populated via JS #}
|
||||
<select id="select-cloud" class="form-input" style="display:none;">
|
||||
<option disabled selected>Loading cloud backups…</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Cloud info notice #}
|
||||
<div id="cloud-notice" style="display:none;margin-top:10px;padding:10px 14px;
|
||||
background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.2);
|
||||
border-radius:8px;font-size:12px;color:var(--accent2);">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
The backup will be downloaded from R2 to the server first, then restored.
|
||||
This may take a few minutes depending on the file size (typically 1–2 GB).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +114,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<button class="btn btn-danger btn-lg" onclick="launchRestore()" id="restore-btn">
|
||||
<button class="btn btn-danger btn-lg" onclick="nvLaunchRestore()" id="restore-btn">
|
||||
<i class="fas fa-play"></i> Start Restore
|
||||
</button>
|
||||
<p style="color:var(--text3); font-size:12px; margin-top:8px;">⚠ Healthy running containers are skipped.</p>
|
||||
@@ -100,7 +130,158 @@
|
||||
<div style="color:var(--text3);font-size:11px;margin-top:6px;font-family:var(--mono);" id="restore-elapsed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.restorePrefill = {{ restore_prefill|tojson }};
|
||||
|
||||
// ── Cloud backup data (key lookup) ────────────────────────────────────────────
|
||||
let _cloudBackups = [];
|
||||
|
||||
async function loadCloudSelect() {
|
||||
const sel = document.getElementById('select-cloud');
|
||||
try {
|
||||
const res = await fetch('/api/cloud/r2/backups');
|
||||
const data = await res.json();
|
||||
// Only .tar.gz — no .sha256
|
||||
_cloudBackups = (data.backups || []).filter(b => b.name.endsWith('.tar.gz'));
|
||||
if (_cloudBackups.length === 0) {
|
||||
sel.innerHTML = '<option disabled>No cloud backups found</option>';
|
||||
} else {
|
||||
sel.innerHTML = _cloudBackups.map(b =>
|
||||
`<option value="${b.name}" data-key="${b.key}">${b.name} (${b.size_human})</option>`
|
||||
).join('');
|
||||
}
|
||||
} catch(e) {
|
||||
sel.innerHTML = '<option disabled>Failed to load R2 backups</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Show/hide correct select based on source radio ────────────────────────────
|
||||
function nvUpdateSource() {
|
||||
const source = document.querySelector('input[name="backup_source"]:checked')?.value;
|
||||
document.getElementById('select-local').style.display = source === 'local' ? '' : 'none';
|
||||
document.getElementById('select-vm').style.display = source === 'vm' ? '' : 'none';
|
||||
document.getElementById('select-cloud').style.display = source === 'cloud' ? '' : 'none';
|
||||
document.getElementById('cloud-notice').style.display = source === 'cloud' ? '' : 'none';
|
||||
}
|
||||
|
||||
// ── Get currently selected file + source ─────────────────────────────────────
|
||||
function nvGetSelection() {
|
||||
const source = document.querySelector('input[name="backup_source"]:checked')?.value || 'local';
|
||||
let file = '', key = '';
|
||||
if (source === 'local') {
|
||||
file = document.getElementById('select-local').value;
|
||||
} else if (source === 'vm') {
|
||||
file = document.getElementById('select-vm').value;
|
||||
} else {
|
||||
const sel = document.getElementById('select-cloud');
|
||||
file = sel.value;
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
key = opt?.dataset?.key || ('backups/' + file);
|
||||
}
|
||||
return { source, file, key };
|
||||
}
|
||||
|
||||
// ── Launch restore ────────────────────────────────────────────────────────────
|
||||
async function nvLaunchRestore() {
|
||||
const { source, file, key } = nvGetSelection();
|
||||
if (!file) { alert('Please select a backup file.'); return; }
|
||||
|
||||
const target = document.querySelector('input[name="restore_target"]:checked')?.value || 'local';
|
||||
const authMethod = document.querySelector('input[name="auth_method"]:checked')?.value || 'key';
|
||||
|
||||
const body = {
|
||||
backup_source: source,
|
||||
backup_file: file,
|
||||
cloud_key: key,
|
||||
target,
|
||||
remote_ip: document.getElementById('remote-ip')?.value || '',
|
||||
remote_port: document.getElementById('remote-port')?.value || '22',
|
||||
remote_user: document.getElementById('remote-user')?.value || 'root',
|
||||
auth_method: authMethod,
|
||||
ssh_key_path: document.getElementById('ssh-key-path')?.value || '',
|
||||
ssh_password: document.getElementById('ssh-password')?.value || '',
|
||||
};
|
||||
|
||||
const btn = document.getElementById('restore-btn');
|
||||
const wrapper = document.getElementById('restore-log-wrapper');
|
||||
const logEl = document.getElementById('restore-log');
|
||||
const badge = document.getElementById('restore-status-badge');
|
||||
const elapsed = document.getElementById('restore-elapsed');
|
||||
|
||||
btn.disabled = true;
|
||||
wrapper.style.display = '';
|
||||
logEl.innerHTML = source === 'cloud'
|
||||
? '<div style="color:var(--accent2)">☁️ Downloading backup from R2, please wait…</div>'
|
||||
: '<div style="color:var(--accent2)">🚀 Starting restore…</div>';
|
||||
badge.textContent = 'Running…';
|
||||
badge.style.cssText = 'background:rgba(59,130,246,0.15);color:var(--accent2);';
|
||||
|
||||
try {
|
||||
const res = await fetch('/restore/start', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
logEl.innerHTML += `<div style="color:#f87171;">❌ ${data.error}</div>`;
|
||||
badge.textContent = '❌ Error';
|
||||
badge.style.cssText = 'background:rgba(239,68,68,0.12);color:#f87171;';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const jobId = data.job_id;
|
||||
const poll = setInterval(async () => {
|
||||
try {
|
||||
const s = await fetch(`/restore/status/${jobId}`);
|
||||
const job = await s.json();
|
||||
logEl.innerHTML = job.log.map(l => `<div>${l}</div>`).join('');
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
elapsed.textContent = `Elapsed: ${job.elapsed}s`;
|
||||
if (job.status === 'done') {
|
||||
clearInterval(poll);
|
||||
badge.textContent = '✅ Done';
|
||||
badge.style.cssText = 'background:rgba(34,197,94,0.12);color:var(--green);';
|
||||
btn.disabled = false;
|
||||
} else if (job.status === 'error') {
|
||||
clearInterval(poll);
|
||||
badge.textContent = '❌ Failed';
|
||||
badge.style.cssText = 'background:rgba(239,68,68,0.12);color:#f87171;';
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch(e) { clearInterval(poll); }
|
||||
}, 1500);
|
||||
} catch(e) {
|
||||
logEl.innerHTML += `<div style="color:#f87171;">❌ ${e.message}</div>`;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadCloudSelect();
|
||||
|
||||
// Handle prefill from URL params (e.g. clicking Restore from backups page)
|
||||
const pf = window.restorePrefill || {};
|
||||
if (pf.source) {
|
||||
const radio = document.querySelector(`input[name="backup_source"][value="${pf.source}"]`);
|
||||
if (radio) { radio.checked = true; nvUpdateSource(); }
|
||||
}
|
||||
if (pf.file) {
|
||||
// Pre-select file after a short delay to let cloud options load
|
||||
setTimeout(() => {
|
||||
const source = pf.source || 'local';
|
||||
const selId = source === 'cloud' ? 'select-cloud' : source === 'vm' ? 'select-vm' : 'select-local';
|
||||
const sel = document.getElementById(selId);
|
||||
if (sel) {
|
||||
for (let o of sel.options) {
|
||||
if (o.value === pf.file) { o.selected = true; break; }
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user