Add Backups tab: daily cron + on-demand trigger + download, 7-day retention

This commit is contained in:
2026-05-30 05:12:13 +00:00
parent 53b1c6b90a
commit d0f751372c
+143
View File
@@ -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)}
<div id="sidebar">
<div class="nav-section">OVERVIEW</div>
<div class="nav-item active" data-tab="dashboard" onclick="nav(this)">DASHBOARD</div>
<div class="nav-item" data-tab="backups" onclick="nav(this)">💾 BACKUPS</div>
<div class="nav-section">MANAGE</div>
<div class="nav-item" data-tab="agents" onclick="nav(this)">AGENTS</div>
<div class="nav-item" data-tab="network" onclick="nav(this)">NETWORK</div>
@@ -618,6 +660,25 @@ select.filter-sel:focus{border-color:var(--cyan)}
<div class="tbl-wrap" id="vms-tbl"><div class="loading">SCANNING...</div></div>
</div>
<!-- BACKUPS -->
<div class="tab" id="tab-backups">
<div class="page-title">BACKUPS
<div class="actions">
<button class="btn btn-sm btn-green" id="backupRunBtn" onclick="triggerBackup()">▶ RUN BACKUP NOW</button>
<button class="btn btn-sm" onclick="loadBackups()">REFRESH</button>
</div>
</div>
<div id="backup-status-bar" style="display:none;background:var(--panel);border:1px solid var(--border2);padding:12px 16px;margin-bottom:16px;font-size:0.7rem">
<span style="color:var(--yellow);letter-spacing:1px" id="backup-status-msg">BACKUP RUNNING...</span>
<div style="margin-top:6px;height:3px;background:var(--border)"><div id="backup-progress-bar" style="height:100%;background:var(--yellow);width:0%;transition:width 1s"></div></div>
<div style="color:var(--dim);font-size:0.65rem;margin-top:6px" id="backup-log-tail"></div>
</div>
<div style="color:var(--dim);font-size:0.65rem;margin-bottom:16px">
Daily automatic backup runs at 2:00 AM. Files + all databases. Last 7 days retained. Stored on server — download anytime.
</div>
<div id="backups-list"><div class="loading">SCANNING...</div></div>
</div>
<!-- SITES -->
<div class="tab" id="tab-sites">
<div class="page-title">SITE HEALTH</div>
@@ -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() {
<tbody>${html}</tbody></table>`;
}
// ── 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 = '<div class="loading">SCANNING...</div>';
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 = '<div class="empty">NO BACKUPS YET — click RUN BACKUP NOW to create the first one</div>';
return;
}
list.innerHTML = `<table>
<thead><tr><th>DATE / TIME</th><th>FILENAME</th><th>SIZE</th><th>DOWNLOAD</th></tr></thead>
<tbody>${files.map((f, i) => `<tr class="agent-row" style="animation-delay:${i*60}ms">
<td style="color:${i===0?'var(--cyan)':'var(--text)'}">${f.date}${i===0?' <span style="font-size:0.55rem;color:var(--green)">● LATEST</span>':''}</td>
<td style="font-size:0.65rem;color:var(--dim)">${esc(f.file)}</td>
<td>${fmtSize(f.size)}</td>
<td><a href="?action=backup_download&file=${encodeURIComponent(f.file)}" class="btn btn-sm btn-green" style="display:inline-block;padding:4px 12px;font-size:0.65rem" download="${esc(f.file)}">↓ DOWNLOAD</a></td>
</tr>`).join('')}</tbody></table>`;
}
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) ───────────────────────────────────────────
<?php if (loggedIn()): ?>
document.getElementById('loginWrap').style.display='none';