Sync from main server - 2026-05-13 01:06:32

This commit is contained in:
root
2026-05-13 01:06:32 +02:00
parent 09bbe0403c
commit 6158b34613
8 changed files with 2159 additions and 129 deletions

View File

@@ -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 12 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 %}