287 lines
13 KiB
HTML
287 lines
13 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title"><i class="fas fa-rotate-right"></i> Restore Configuration</div>
|
||
</div>
|
||
|
||
<div class="restore-form">
|
||
<div class="form-section">
|
||
<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="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="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>
|
||
|
||
{# 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>
|
||
|
||
<div class="form-section">
|
||
<div class="form-section-title">STEP 2 — SELECT RESTORE TARGET</div>
|
||
<div class="radio-group">
|
||
<label class="radio-card">
|
||
<input type="radio" name="restore_target" value="local" checked onchange="toggleRemoteFields()">
|
||
<div class="radio-body"><span class="radio-icon">🎯</span><div><div class="radio-label">Restore on This Server</div><div class="radio-desc" id="this-server-desc">Loading hostname…</div></div></div>
|
||
</label>
|
||
<label class="radio-card">
|
||
<input type="radio" name="restore_target" value="remote" onchange="toggleRemoteFields()">
|
||
<div class="radio-body"><span class="radio-icon">📡</span><div><div class="radio-label">External Machine</div><div class="radio-desc">via SSH — any IP</div></div></div>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="remote-fields" style="display:none; margin-top:16px; max-width:560px;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">TARGET IP</label>
|
||
<input type="text" id="remote-ip" class="form-input" placeholder="192.168.x.x or IP">
|
||
</div>
|
||
<div class="form-group" style="max-width:100px;">
|
||
<label class="form-label">SSH PORT</label>
|
||
<input type="text" id="remote-port" class="form-input" value="22">
|
||
</div>
|
||
<div class="form-group" style="max-width:120px;">
|
||
<label class="form-label">SSH USER</label>
|
||
<input type="text" id="remote-user" class="form-input" value="root">
|
||
</div>
|
||
</div>
|
||
<div class="form-group" style="margin-top:8px;">
|
||
<label class="form-label">AUTHENTICATION</label>
|
||
<div class="radio-group" style="gap:8px;">
|
||
<label class="radio-card small">
|
||
<input type="radio" name="auth_method" value="key" checked onchange="toggleAuthFields()">
|
||
<div class="radio-body"><span>🔑</span><div class="radio-label">SSH Key</div></div>
|
||
</label>
|
||
<label class="radio-card small">
|
||
<input type="radio" name="auth_method" value="password" onchange="toggleAuthFields()">
|
||
<div class="radio-body"><span>🔒</span><div class="radio-label">Password</div></div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div id="key-field" class="form-group" style="margin-top:8px;">
|
||
<label class="form-label">SSH KEY PATH</label>
|
||
<input type="text" id="ssh-key-path" class="form-input" value="/root/.ssh/contabo-key">
|
||
</div>
|
||
<div id="password-field" class="form-group" style="display:none; margin-top:8px;">
|
||
<label class="form-label">SSH PASSWORD</label>
|
||
<input type="password" id="ssh-password" class="form-input">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="restore-log-wrapper" style="display:none; margin-top:20px;">
|
||
<div class="card-header" style="margin-bottom:10px;">
|
||
<div class="card-title"><i class="fas fa-terminal"></i> Restore Log</div>
|
||
<span class="badge" id="restore-status-badge" style="background:rgba(59,130,246,0.15);color:var(--accent2);">Running…</span>
|
||
</div>
|
||
<div id="restore-log" class="log-console"></div>
|
||
<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 %} |