diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 6558168..c2e9c75 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -312,6 +312,47 @@ if ($action) { } j(['ok' => true]); + // ── BACKUPS ─────────────────────────────────────────────────────────── + case 'backups_list': + $dir = '/var/backups/jarvis'; + $lock = "$dir/backup.lock"; + $log = "$dir/backup.log"; + $running = file_exists($lock) && (time() - filemtime($lock)) < 3600; + $files = []; + foreach (glob("$dir/jarvis_backup_*.tar.gz") ?: [] as $f) { + $files[] = [ + 'file' => basename($f), + 'size' => filesize($f), + 'size_mb' => round(filesize($f)/1048576, 1), + 'date' => date('Y-m-d H:i:s', filemtime($f)), + ]; + } + usort($files, fn($a,$b) => strcmp($b['date'], $a['date'])); + $lastLog = $log && file_exists($log) ? trim(shell_exec("tail -3 " . escapeshellarg($log))) : ''; + j(['running' => $running, 'files' => $files, 'last_log' => $lastLog]); + + case 'backup_trigger': + $lock = '/var/backups/jarvis/backup.lock'; + if (file_exists($lock) && (time() - filemtime($lock)) < 3600) { + j(['ok' => false, 'message' => 'Backup already running']); + } + shell_exec('nohup /usr/local/bin/jarvis-backup.sh > /dev/null 2>&1 &'); + sleep(1); + j(['ok' => true, 'message' => 'Backup started']); + + case 'backup_download': + $file = basename($_GET['file'] ?? ''); + if (!preg_match('/^jarvis_backup_[\d_-]+\.tar\.gz$/', $file)) bad('Invalid filename'); + $path = '/var/backups/jarvis/' . $file; + if (!file_exists($path)) bad('File not found', 404); + header('Content-Type: application/gzip'); + header('Content-Disposition: attachment; filename="' . $file . '"'); + header('Content-Length: ' . filesize($path)); + header('X-Accel-Buffering: no'); + ob_end_clean(); + readfile($path); + exit; + default: bad('Unknown action'); } } @@ -475,6 +516,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + +
+
BACKUPS +
+ + +
+
+ +
+ Daily automatic backup runs at 2:00 AM. Files + all databases. Last 7 days retained. Stored on server — download anytime. +
+
SCANNING...
+
+
SITE HEALTH
@@ -728,6 +789,7 @@ function loadTab(tab) { // Stop any existing network auto-refresh when leaving if (_netAutoRefresh) { clearInterval(_netAutoRefresh); _netAutoRefresh = null; } ({ + backups: loadBackups, dashboard: loadDashboard, agents: loadAgents, network: ()=>{ loadNetwork(); _netAutoRefresh = setInterval(loadNetwork, 30000); }, @@ -1353,6 +1415,87 @@ async function loadVMs() { ${html}`; } +// ── BACKUPS ──────────────────────────────────────────────────────────────────── +let _backupPollTimer = null; + +function fmtSize(bytes) { + if (bytes >= 1073741824) return (bytes/1073741824).toFixed(1) + ' GB'; + if (bytes >= 1048576) return (bytes/1048576).toFixed(1) + ' MB'; + return (bytes/1024).toFixed(0) + ' KB'; +} + +async function loadBackups() { + const list = document.getElementById('backups-list'); + list.innerHTML = '
SCANNING...
'; + const data = await api('backups_list'); + + // Show/hide running status bar + const bar = document.getElementById('backup-status-bar'); + if (data.running) { + bar.style.display = 'block'; + document.getElementById('backup-status-msg').textContent = 'BACKUP IN PROGRESS...'; + document.getElementById('backup-log-tail').textContent = data.last_log || ''; + // Animate progress bar + let pct = parseInt(document.getElementById('backup-progress-bar').style.width) || 5; + pct = Math.min(pct + 8, 90); + document.getElementById('backup-progress-bar').style.width = pct + '%'; + document.getElementById('backup-progress-bar').style.background = 'var(--yellow)'; + if (!_backupPollTimer) _backupPollTimer = setInterval(loadBackups, 4000); + } else { + if (_backupPollTimer) { clearInterval(_backupPollTimer); _backupPollTimer = null; } + if (bar.style.display !== 'none') { + // Just finished + document.getElementById('backup-status-msg').textContent = '✓ BACKUP COMPLETE'; + document.getElementById('backup-progress-bar').style.width = '100%'; + document.getElementById('backup-progress-bar').style.background = 'var(--green)'; + document.getElementById('backup-log-tail').textContent = data.last_log || ''; + setTimeout(() => { bar.style.display = 'none'; }, 4000); + } + document.getElementById('backupRunBtn').disabled = false; + document.getElementById('backupRunBtn').textContent = '▶ RUN BACKUP NOW'; + } + + const files = data.files || []; + if (!files.length) { + list.innerHTML = '
NO BACKUPS YET — click RUN BACKUP NOW to create the first one
'; + return; + } + + list.innerHTML = ` + + ${files.map((f, i) => ` + + + + + `).join('')}
DATE / TIMEFILENAMESIZEDOWNLOAD
${f.date}${i===0?' ● LATEST':''}${esc(f.file)}${fmtSize(f.size)}↓ DOWNLOAD
`; +} + +async function triggerBackup() { + const btn = document.getElementById('backupRunBtn'); + btn.disabled = true; btn.textContent = 'STARTING...'; + const bar = document.getElementById('backup-status-bar'); + bar.style.display = 'block'; + document.getElementById('backup-status-msg').textContent = 'BACKUP STARTING...'; + document.getElementById('backup-progress-bar').style.width = '5%'; + document.getElementById('backup-progress-bar').style.background = 'var(--yellow)'; + document.getElementById('backup-log-tail').textContent = ''; + + const fd = new FormData(); fd.append('action','backup_trigger'); + try { + const r = await fetch(location.href, {method:'POST', body:fd}); + const d = await r.json(); + if (d.ok) { + toast('Backup started — polling for completion...', 'ok'); + btn.textContent = 'RUNNING...'; + if (!_backupPollTimer) _backupPollTimer = setInterval(loadBackups, 4000); + } else { + toast(d.message || 'Already running', 'ok'); + btn.disabled = false; btn.textContent = '▶ RUN BACKUP NOW'; + } + } catch(e) { toast('Failed to start backup', 'err'); btn.disabled = false; btn.textContent = '▶ RUN BACKUP NOW'; } +} + // ── AUTO-LOGIN CHECK (PHP session) ─────────────────────────────────────────── document.getElementById('loginWrap').style.display='none';